@sap/cds 9.0.3 → 9.1.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.
- package/CHANGELOG.md +36 -0
- package/bin/serve.js +11 -9
- package/lib/compile/for/lean_drafts.js +29 -7
- package/lib/dbs/cds-deploy.js +5 -3
- package/lib/env/cds-requires.js +1 -1
- package/lib/env/defaults.js +0 -11
- package/lib/env/schemas/cds-rc.js +214 -6
- package/lib/i18n/locale.js +2 -1
- package/lib/req/request.js +1 -1
- package/lib/req/validate.js +1 -2
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/utils/inflect.js +2 -2
- package/lib/utils/tar.js +60 -23
- package/libx/_runtime/common/generic/crud.js +1 -3
- package/libx/_runtime/common/generic/input.js +2 -2
- package/libx/_runtime/common/generic/temporal.js +0 -6
- package/libx/_runtime/fiori/lean-draft.js +487 -141
- package/libx/_runtime/remote/utils/client.js +1 -0
- package/libx/odata/ODataAdapter.js +47 -43
- package/libx/odata/middleware/batch.js +0 -1
- package/libx/odata/middleware/error.js +7 -0
- package/libx/odata/middleware/operation.js +15 -21
- package/libx/odata/parse/afterburner.js +23 -10
- package/libx/odata/parse/cqn2odata.js +2 -2
- package/libx/odata/parse/grammar.peggy +182 -133
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +0 -35
- package/libx/odata/utils/metadata.js +34 -1
- package/libx/odata/utils/odataBind.js +2 -1
- package/libx/odata/utils/result.js +22 -20
- package/libx/queue/index.js +6 -2
- package/package.json +1 -1
|
@@ -10,12 +10,15 @@ 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]
|
|
@@ -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,93 @@ const _replaceStreams = result => {
|
|
|
299
382
|
}
|
|
300
383
|
}
|
|
301
384
|
|
|
385
|
+
const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestData, draftsRef) => {
|
|
386
|
+
const prefixRef = []
|
|
387
|
+
const simplePathElements = []
|
|
388
|
+
let targetEntity = cds.infer.ref([draftsRef[0]], cds.context.tx.model)
|
|
389
|
+
|
|
390
|
+
// Determine the path prefix required for a fully qualified validation message 'target'
|
|
391
|
+
for (let refIdx = 0; refIdx < draftsRef.length; refIdx++) {
|
|
392
|
+
let dRef = draftsRef[refIdx]
|
|
393
|
+
if (refIdx > 0) targetEntity = targetEntity.elements[draftsRef[refIdx].id || draftsRef[refIdx]]._target
|
|
394
|
+
|
|
395
|
+
const pRef = { where: [] }
|
|
396
|
+
|
|
397
|
+
const dRefId = dRef.id ?? dRef
|
|
398
|
+
if (dRefId === targetEntity.name) {
|
|
399
|
+
if (!targetEntity.isDraft) pRef.id = targetEntity.name.replace(`${targetEntity._service.name}.`, '')
|
|
400
|
+
else pRef.id = targetEntity.actives.name.replace(`${targetEntity.actives._service.name}.`, '')
|
|
401
|
+
} else pRef.id = dRefId
|
|
402
|
+
simplePathElements.push(pRef.id)
|
|
403
|
+
|
|
404
|
+
if (targetEntity.isDraft) targetEntity = targetEntity.actives
|
|
405
|
+
|
|
406
|
+
if (typeof dRef === 'string' || !dRef.where?.length) {
|
|
407
|
+
for (const k in targetEntity.keys) {
|
|
408
|
+
const key = targetEntity.keys[k]
|
|
409
|
+
let v = requestData[key.name]
|
|
410
|
+
if (v === undefined)
|
|
411
|
+
if (key.name === 'IsActiveEntity') v = false
|
|
412
|
+
else return null
|
|
413
|
+
if (pRef.where.length > 0) pRef.where.push('and')
|
|
414
|
+
pRef.where.push(key.name, '=', v)
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
pRef.where.push(...dRef.where, 'and', 'IsActiveEntity', '=', false)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
prefixRef.push(pRef)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const nextMessages = []
|
|
424
|
+
|
|
425
|
+
// Collect messages that were created during the most recent validation run
|
|
426
|
+
const newMessagesByCodeAndTarget = newMessages.reduce((acc, message) => {
|
|
427
|
+
message.simplePathElements = [...simplePathElements]
|
|
428
|
+
|
|
429
|
+
const messagePrefixRef = [...prefixRef]
|
|
430
|
+
const messageTarget = message.target.startsWith('in/') ? message.target.slice(3) : message.target
|
|
431
|
+
const messageTargetRef = cds.odata.parse(messageTarget).SELECT.from.ref
|
|
432
|
+
message.target = messageTargetRef.pop()
|
|
433
|
+
|
|
434
|
+
for (const tRef of messageTargetRef) {
|
|
435
|
+
message.simplePathElements.push(tRef.id)
|
|
436
|
+
messagePrefixRef.push(tRef)
|
|
437
|
+
if ((tRef.where ??= []).some(w => w === 'IsActiveEntity' || w.ref?.[0] === 'IsActiveEntity')) continue
|
|
438
|
+
if (tRef.where.length > 0) tRef.where.push('and')
|
|
439
|
+
tRef.where.push('IsActiveEntity', '=', false)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
message.prefix = cds.odata.urlify({ SELECT: { from: { ref: messagePrefixRef } } }).path
|
|
443
|
+
message.numericSeverity ??= 4
|
|
444
|
+
|
|
445
|
+
nextMessages.push(message)
|
|
446
|
+
|
|
447
|
+
return acc.set(`${message.message}:${message.prefix}:${message.target}`, message)
|
|
448
|
+
}, new Map())
|
|
449
|
+
|
|
450
|
+
const draftMessageTargetPrefix = cds.odata.urlify({ SELECT: { from: { ref: prefixRef } } }).path
|
|
451
|
+
|
|
452
|
+
// Merge new messages with persisted ones & eliminate outdated ones
|
|
453
|
+
for (const message of persistedMessages) {
|
|
454
|
+
if (newMessagesByCodeAndTarget.has(`${message.message}:${message.prefix}:${message.target}`)) continue
|
|
455
|
+
if (message.prefix === draftMessageTargetPrefix && requestData[message.target] !== undefined) continue
|
|
456
|
+
if (
|
|
457
|
+
message.simplePathElements.length > simplePathElements.length &&
|
|
458
|
+
requestData[message.simplePathElements[simplePathElements.length]] !== undefined
|
|
459
|
+
)
|
|
460
|
+
continue
|
|
461
|
+
|
|
462
|
+
nextMessages.push(message)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// REVISIT: Do this by default in validation?
|
|
466
|
+
for (const msg of nextMessages)
|
|
467
|
+
for (const i in msg.args) if (msg.args[i] instanceof RegExp) msg.args[i] = msg.args[i].toString()
|
|
468
|
+
|
|
469
|
+
return nextMessages
|
|
470
|
+
}
|
|
471
|
+
|
|
302
472
|
// REVISIT: Can we do a regular handler function instead of monky patching?
|
|
303
473
|
const h = cds.ApplicationService.prototype.handle
|
|
304
474
|
const handle = async function (req) {
|
|
@@ -365,12 +535,23 @@ const handle = async function (req) {
|
|
|
365
535
|
if (!_req._.event) _req._.event = req.event
|
|
366
536
|
const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
|
|
367
537
|
if (cqnData) _req.data = cqnData // must point to the same object
|
|
538
|
+
if (req.tx && !_req.tx) _req.tx = req.tx
|
|
539
|
+
|
|
540
|
+
// Ensure messages are added to the original request
|
|
368
541
|
Object.defineProperty(_req, '_messages', {
|
|
369
542
|
get: function () {
|
|
370
543
|
return req._messages
|
|
371
544
|
}
|
|
372
545
|
})
|
|
373
|
-
|
|
546
|
+
|
|
547
|
+
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
|
|
548
|
+
if (_req.target.isDraft && (_req.event === 'UPDATE' || _req.event === 'NEW')) {
|
|
549
|
+
// Degrade all errors to messages & prevent !!req.errors into req.reject() in dispatch
|
|
550
|
+
_req.error = (...args) => {
|
|
551
|
+
for (const err of args) _req._messages.add(4, err)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
374
555
|
|
|
375
556
|
return _req
|
|
376
557
|
}
|
|
@@ -447,6 +628,7 @@ const handle = async function (req) {
|
|
|
447
628
|
typeof query.INSERT.into === 'string'
|
|
448
629
|
? [req.target.drafts.name]
|
|
449
630
|
: _redirectRefToDrafts([query.INSERT.into.ref[0]], this.model)
|
|
631
|
+
|
|
450
632
|
let rootHasDraft
|
|
451
633
|
|
|
452
634
|
// children: check root entity has no draft
|
|
@@ -478,6 +660,33 @@ const handle = async function (req) {
|
|
|
478
660
|
if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
|
|
479
661
|
if (!req.target.isDraft) req.target = req.target.drafts // COMPAT: also support these events for actives
|
|
480
662
|
|
|
663
|
+
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && query.DELETE) {
|
|
664
|
+
const prefixRef = query.DELETE.from.ref.map(dRef => {
|
|
665
|
+
const pRef = { id: dRef.id, where: [...dRef.where] }
|
|
666
|
+
pRef.where.push('and', 'IsActiveEntity', '=', false)
|
|
667
|
+
return pRef
|
|
668
|
+
})
|
|
669
|
+
const messageTargetPrefix = cds.odata.urlify({ SELECT: { from: { ref: prefixRef } } }).path
|
|
670
|
+
|
|
671
|
+
const draftData = await SELECT.one
|
|
672
|
+
.from({ ref: _redirectRefToDrafts(query.DELETE.from.ref, this.model) }) // REVISIT: Avoid redundant redirect
|
|
673
|
+
.columns('DraftAdministrativeData_DraftUUID', {
|
|
674
|
+
ref: ['DraftAdministrativeData'],
|
|
675
|
+
expand: [{ ref: ['DraftMessages'] }]
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
const draftAdminDataUUID = draftData?.DraftAdministrativeData_DraftUUID
|
|
679
|
+
const persistedDraftMessages = draftData?.DraftAdministrativeData?.DraftMessages || []
|
|
680
|
+
|
|
681
|
+
if (draftAdminDataUUID && persistedDraftMessages?.length) {
|
|
682
|
+
const nextDraftMessages = persistedDraftMessages.filter(msg => !msg.prefix.startsWith(messageTargetPrefix))
|
|
683
|
+
|
|
684
|
+
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
685
|
+
.set({ DraftMessages: nextDraftMessages })
|
|
686
|
+
.where({ DraftUUID: draftAdminDataUUID })
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
481
690
|
if (query.INSERT) {
|
|
482
691
|
if (typeof query.INSERT.into === 'string') query.INSERT.into = req.target.name
|
|
483
692
|
else if (query.INSERT.into.ref) query.INSERT.into.ref = _redirectRefToDrafts(query.INSERT.into.ref, this.model)
|
|
@@ -490,15 +699,12 @@ const handle = async function (req) {
|
|
|
490
699
|
const _req = _newReq(req, query, draftParams, { event: req.event })
|
|
491
700
|
|
|
492
701
|
// 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
|
-
) {
|
|
702
|
+
if (req.event === 'NEW' && draftParams.IsActiveEntity === false && !_req.target.isDraft) {
|
|
498
703
|
req.reject({ code: 403, statusCode: 403, message: 'ACTIVE_MODIFICATION_VIA_DRAFT' })
|
|
499
704
|
}
|
|
500
705
|
|
|
501
706
|
const result = await h.call(this, _req)
|
|
707
|
+
|
|
502
708
|
req.data = result //> make keys available via req.data (as with normal crud)
|
|
503
709
|
return result
|
|
504
710
|
}
|
|
@@ -506,13 +712,13 @@ const handle = async function (req) {
|
|
|
506
712
|
// Delete active instance of draft-enabled entity
|
|
507
713
|
if (req.target.drafts && !req.target.isDraft && req.event === 'DELETE' && draftParams.IsActiveEntity !== false) {
|
|
508
714
|
const draftsRef = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
715
|
+
const inProcessByUserCol = {
|
|
716
|
+
ref: ['DraftAdministrativeData'],
|
|
717
|
+
expand: [_inProcessByUserXpr(_lock.shiftedNow)]
|
|
718
|
+
}
|
|
719
|
+
const draftQuery = SELECT.one
|
|
720
|
+
.from({ ref: draftsRef })
|
|
721
|
+
.columns([{ ref: ['DraftAdministrativeData_DraftUUID'] }, inProcessByUserCol])
|
|
516
722
|
if (query.DELETE.where) draftQuery.where(query.DELETE.where)
|
|
517
723
|
|
|
518
724
|
// Deletion of active instance outside draft tree, no need to check for draft
|
|
@@ -523,11 +729,59 @@ const handle = async function (req) {
|
|
|
523
729
|
}
|
|
524
730
|
|
|
525
731
|
// Deletion of active instance inside draft tree, need to check that no draft exists
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
732
|
+
const drafts = []
|
|
733
|
+
const draftsRes = await draftQuery
|
|
734
|
+
if (draftsRes) drafts.push(draftsRes)
|
|
735
|
+
|
|
736
|
+
// For hierarchies, check that no sub node exists.
|
|
737
|
+
if (target?.elements?.LimitedDescendantCount) {
|
|
738
|
+
let key
|
|
739
|
+
for (const _key in req.target.keys) {
|
|
740
|
+
if (_key === 'IsActiveEntity') continue
|
|
741
|
+
key = _key // only single key supported
|
|
742
|
+
}
|
|
743
|
+
// We must only do this for recursive _composition_ children.
|
|
744
|
+
// For recursive _association_ children, app developers must deal with dangling pointers themselves.
|
|
745
|
+
const _recursiveComposition = target => {
|
|
746
|
+
for (const _comp in target.compositions) {
|
|
747
|
+
if (target.compositions[_comp]['@odata.draft.ignore']) {
|
|
748
|
+
return target.compositions[_comp]
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const recursiveComposition = _recursiveComposition(req.target)
|
|
753
|
+
if (recursiveComposition) {
|
|
754
|
+
let uplinkName
|
|
755
|
+
for (const key in req.target) {
|
|
756
|
+
if (key.match(/@Aggregation\.RecursiveHierarchy\s*#.*\.ParentNavigationProperty/)) {
|
|
757
|
+
uplinkName = req.target[key]['=']
|
|
758
|
+
break
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const keyVal = req.query.DELETE.from.ref[0].where?.[2]?.val
|
|
762
|
+
if (keyVal === undefined) req.reject(400, 'Deletion not supported')
|
|
763
|
+
// We must select actives and check for corresponding drafts (drafts themselve don't necessarily form a hierarchy)
|
|
764
|
+
const recursiveQ = SELECT.from(req.target).columns(key)
|
|
765
|
+
recursiveQ.SELECT.recurse = {
|
|
766
|
+
ref: [uplinkName],
|
|
767
|
+
where: [{ func: 'DistanceTo', args: [{ val: keyVal }, { val: null }] }]
|
|
768
|
+
}
|
|
769
|
+
const recursives = await recursiveQ
|
|
770
|
+
if (recursives.length) {
|
|
771
|
+
const recursiveDrafts = await SELECT.from(req.target.drafts)
|
|
772
|
+
.columns(inProcessByUserCol)
|
|
773
|
+
.where(Read.whereIn(req.target, recursives))
|
|
774
|
+
drafts.push(...recursiveDrafts)
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
for (const draft of drafts) {
|
|
780
|
+
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
781
|
+
if (!cds.context.user._is_privileged && inProcessByUser && inProcessByUser !== cds.context.user.id)
|
|
782
|
+
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [inProcessByUser] })
|
|
783
|
+
else req.reject({ code: 403, statusCode: 403, message: 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS' })
|
|
784
|
+
}
|
|
531
785
|
await run(query)
|
|
532
786
|
return req.data
|
|
533
787
|
}
|
|
@@ -535,8 +789,6 @@ const handle = async function (req) {
|
|
|
535
789
|
if (req.event === 'draftActivate') {
|
|
536
790
|
LOG.debug('activate draft')
|
|
537
791
|
|
|
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
792
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity === true) {
|
|
541
793
|
req.reject({
|
|
542
794
|
code: 400,
|
|
@@ -549,21 +801,29 @@ const handle = async function (req) {
|
|
|
549
801
|
req.reject({ code: 428, statusCode: 428 })
|
|
550
802
|
}
|
|
551
803
|
|
|
552
|
-
const
|
|
804
|
+
const columns = expandStarStar(req.target.drafts, true)
|
|
553
805
|
|
|
554
|
-
// Use `run` (since also etags might need to be checked)
|
|
555
|
-
// REVISIT: Find a better approach (`etag` as part of CQN?)
|
|
556
806
|
const draftRef = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
557
807
|
const draftQuery = SELECT.one
|
|
558
808
|
.from({ ref: draftRef })
|
|
559
|
-
.columns(
|
|
809
|
+
.columns(columns)
|
|
560
810
|
.columns([
|
|
561
|
-
'
|
|
562
|
-
'
|
|
563
|
-
{ ref: ['
|
|
811
|
+
// Will automatically select 'IsActiveEntity' as key column
|
|
812
|
+
{ ref: ['HasActiveEntity'] },
|
|
813
|
+
{ ref: ['DraftAdministrativeData_DraftUUID'] },
|
|
814
|
+
{
|
|
815
|
+
ref: ['DraftAdministrativeData'],
|
|
816
|
+
expand: [
|
|
817
|
+
{ ref: ['InProcessByUser'] },
|
|
818
|
+
...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
|
|
819
|
+
? [{ ref: ['DraftMessages'] }]
|
|
820
|
+
: [])
|
|
821
|
+
]
|
|
822
|
+
}
|
|
564
823
|
])
|
|
565
824
|
.where(query.SELECT.where)
|
|
566
825
|
const res = await run(draftQuery)
|
|
826
|
+
|
|
567
827
|
if (!res) {
|
|
568
828
|
const _etagValidationType = req.headers['if-match']
|
|
569
829
|
? 'if-match'
|
|
@@ -581,23 +841,41 @@ const handle = async function (req) {
|
|
|
581
841
|
})
|
|
582
842
|
}
|
|
583
843
|
|
|
844
|
+
// Remove draft artefacts from persistedDraft entry
|
|
584
845
|
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
585
846
|
delete res.DraftAdministrativeData_DraftUUID
|
|
586
847
|
delete res.DraftAdministrativeData
|
|
587
848
|
const HasActiveEntity = res.HasActiveEntity
|
|
588
849
|
delete res.HasActiveEntity
|
|
589
850
|
|
|
590
|
-
if (
|
|
851
|
+
if (
|
|
852
|
+
_hasStreaming(draftQuery.SELECT.columns, draftQuery._target, true) &&
|
|
853
|
+
!cds.env.features.binary_draft_compat &&
|
|
854
|
+
!cds.env.fiori.move_media_data_in_db
|
|
855
|
+
) {
|
|
591
856
|
await _removeEmptyStreams(res)
|
|
857
|
+
}
|
|
592
858
|
|
|
593
859
|
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
594
860
|
const activesRef = _redirectRefToActives(query.SELECT.from.ref, this.model)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
)
|
|
861
|
+
|
|
862
|
+
// Upsert draft into active, not considering media data columns
|
|
863
|
+
const upsertQuery = HasActiveEntity
|
|
864
|
+
? UPDATE({ ref: activesRef }).data(res).where(query.SELECT.where)
|
|
865
|
+
: INSERT.into({ ref: activesRef }).entries(res)
|
|
866
|
+
const upsertOptions = { headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
|
|
867
|
+
|
|
868
|
+
const _req = _newReq(req, upsertQuery, draftParams, upsertOptions)
|
|
869
|
+
const result = await h.call(this, _req)
|
|
870
|
+
|
|
871
|
+
// REVISIT: Remove feature flag dependency
|
|
872
|
+
if (cds.env.fiori.move_media_data_in_db) {
|
|
873
|
+
// Move cds.LargeBinary data from draft to active
|
|
874
|
+
const updateMediaDataQueries = getUpdateFromSelectQueries(req.target, draftRef, activesRef)
|
|
875
|
+
await _promiseAll(updateMediaDataQueries)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Delete draft artefacts
|
|
601
879
|
await _promiseAll([
|
|
602
880
|
DELETE.from({ ref: draftRef }).where(query.SELECT.where),
|
|
603
881
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
@@ -656,15 +934,23 @@ const handle = async function (req) {
|
|
|
656
934
|
}
|
|
657
935
|
}
|
|
658
936
|
_rmIsActiveEntity(req.data, req.target)
|
|
937
|
+
|
|
659
938
|
if (draftParams.IsActiveEntity === false) {
|
|
660
939
|
LOG.debug('patch draft')
|
|
661
940
|
|
|
662
941
|
if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject({ code: 405, statusCode: 405 })
|
|
942
|
+
|
|
663
943
|
const draftsRef = _redirectRefToDrafts(query.UPDATE.entity.ref, this.model)
|
|
664
944
|
const res = await SELECT.one.from({ ref: draftsRef }).columns('DraftAdministrativeData_DraftUUID', {
|
|
665
945
|
ref: ['DraftAdministrativeData'],
|
|
666
|
-
expand: [
|
|
946
|
+
expand: [
|
|
947
|
+
{ ref: ['InProcessByUser'] },
|
|
948
|
+
...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
|
|
949
|
+
? [{ ref: ['DraftMessages'] }]
|
|
950
|
+
: [])
|
|
951
|
+
]
|
|
667
952
|
})
|
|
953
|
+
|
|
668
954
|
if (!res) req.reject({ code: 404, statusCode: 404 })
|
|
669
955
|
if (!cds.context.user._is_privileged && res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
670
956
|
req.reject({
|
|
@@ -674,15 +960,31 @@ const handle = async function (req) {
|
|
|
674
960
|
args: [res.DraftAdministrativeData?.InProcessByUser]
|
|
675
961
|
})
|
|
676
962
|
}
|
|
963
|
+
|
|
964
|
+
await run(UPDATE({ ref: draftsRef }).data(req.data))
|
|
965
|
+
|
|
966
|
+
const nextDraftAdminData = {
|
|
967
|
+
InProcessByUser: req.user.id,
|
|
968
|
+
LastChangedByUser: req.user.id,
|
|
969
|
+
LastChangeDateTime: new Date()
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
|
|
973
|
+
nextDraftAdminData.DraftMessages = _compileUpdatedDraftMessages(
|
|
974
|
+
req.messages ?? [],
|
|
975
|
+
res.DraftAdministrativeData?.DraftMessages ?? [],
|
|
976
|
+
req.data ?? {},
|
|
977
|
+
draftsRef
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
// Prevent validation errors from being sent in 'sap-messages' header
|
|
981
|
+
req.messages = req._set('_messages', new Responses())
|
|
982
|
+
}
|
|
983
|
+
|
|
677
984
|
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
678
|
-
.data(
|
|
679
|
-
InProcessByUser: req.user.id,
|
|
680
|
-
LastChangedByUser: req.user.id,
|
|
681
|
-
LastChangeDateTime: new Date()
|
|
682
|
-
})
|
|
985
|
+
.data(nextDraftAdminData)
|
|
683
986
|
.where({ DraftUUID: res.DraftAdministrativeData_DraftUUID })
|
|
684
987
|
|
|
685
|
-
await run(UPDATE({ ref: draftsRef }).data(req.data))
|
|
686
988
|
req.data.IsActiveEntity = false
|
|
687
989
|
return req.data
|
|
688
990
|
}
|
|
@@ -877,6 +1179,16 @@ const Read = {
|
|
|
877
1179
|
}
|
|
878
1180
|
return result
|
|
879
1181
|
}
|
|
1182
|
+
|
|
1183
|
+
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
|
|
1184
|
+
// Replace selection of 'DraftMessages' with the proper path expression
|
|
1185
|
+
|
|
1186
|
+
query._drafts.SELECT.columns = [
|
|
1187
|
+
...(query._drafts.SELECT.columns ?? ['*']).filter(c => c.ref?.[0] !== 'DraftMessages'),
|
|
1188
|
+
{ ref: ['DraftAdministrativeData', 'DraftMessages'], as: 'DraftMessages' }
|
|
1189
|
+
]
|
|
1190
|
+
}
|
|
1191
|
+
|
|
880
1192
|
const draftsQuery = query._drafts.where(
|
|
881
1193
|
{ ref: ['DraftAdministrativeData', 'InProcessByUser'] },
|
|
882
1194
|
'=',
|
|
@@ -889,7 +1201,31 @@ const Read = {
|
|
|
889
1201
|
HasDraftEntity: false
|
|
890
1202
|
})
|
|
891
1203
|
_fillIsActiveEntity(row, false, query._drafts._target)
|
|
1204
|
+
|
|
1205
|
+
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && row.DraftMessages?.length) {
|
|
1206
|
+
const simplifiedMessagePrefix = query._drafts.SELECT.from.ref
|
|
1207
|
+
.map(refElement => {
|
|
1208
|
+
refElement = (refElement.id ?? refElement).split('.')
|
|
1209
|
+
if (refElement.length === 1) return refElement[0]
|
|
1210
|
+
if (refElement[refElement.length - 1] === 'drafts') return refElement[refElement.length - 2]
|
|
1211
|
+
return refElement[refElement.length - 1]
|
|
1212
|
+
})
|
|
1213
|
+
.join('/')
|
|
1214
|
+
|
|
1215
|
+
row.DraftMessages = row.DraftMessages.reduce((acc, msg) => {
|
|
1216
|
+
if (!msg.simplePathElements.join('/').startsWith(simplifiedMessagePrefix)) return acc
|
|
1217
|
+
msg.target = `/${msg.prefix}/${msg.target}`
|
|
1218
|
+
delete msg.prefix
|
|
1219
|
+
delete msg.simplePathElements
|
|
1220
|
+
acc.push(msg)
|
|
1221
|
+
return acc
|
|
1222
|
+
}, [])
|
|
1223
|
+
|
|
1224
|
+
// TODO: Messsages use i18n to map codes to textual error messages -> Replace!
|
|
1225
|
+
row.DraftMessages = getLocalizedMessages(row.DraftMessages, cds.context.http.req)
|
|
1226
|
+
}
|
|
892
1227
|
})
|
|
1228
|
+
|
|
893
1229
|
return _requested(drafts, query)
|
|
894
1230
|
},
|
|
895
1231
|
|
|
@@ -1229,6 +1565,7 @@ function _cleanseCols(columns, elements, target) {
|
|
|
1229
1565
|
})
|
|
1230
1566
|
}
|
|
1231
1567
|
|
|
1568
|
+
let DRAFT_ADMIN_ELEMENTS
|
|
1232
1569
|
/**
|
|
1233
1570
|
* Creates a clone of the query, cleanses and collects all draft parameters into DRAFT_PARAMS.
|
|
1234
1571
|
*/
|
|
@@ -1355,6 +1692,11 @@ function _cleansed(query, model) {
|
|
|
1355
1692
|
}
|
|
1356
1693
|
|
|
1357
1694
|
function _tweakAdminCols(columns) {
|
|
1695
|
+
if (!DRAFT_ADMIN_ELEMENTS) {
|
|
1696
|
+
DRAFT_ADMIN_ELEMENTS = []
|
|
1697
|
+
for (const key in cds.model.definitions['DRAFT.DraftAdministrativeData'].elements) DRAFT_ADMIN_ELEMENTS.push(key)
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1358
1700
|
if (!columns || columns.some(c => c === '*')) columns = DRAFT_ADMIN_ELEMENTS.map(k => ({ ref: [k] }))
|
|
1359
1701
|
return columns.map(col => {
|
|
1360
1702
|
const name = col.ref?.[0]
|
|
@@ -1444,59 +1786,40 @@ function _cleansed(query, model) {
|
|
|
1444
1786
|
}
|
|
1445
1787
|
}
|
|
1446
1788
|
|
|
1447
|
-
// This function is better defined on DB layer
|
|
1789
|
+
// REVISIT: This function is better defined on DB layer
|
|
1448
1790
|
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
1791
|
const columns = []
|
|
1792
|
+
|
|
1451
1793
|
for (const el in target.elements) {
|
|
1452
1794
|
const element = target.elements[el]
|
|
1453
1795
|
|
|
1454
|
-
//
|
|
1796
|
+
// Skip calculated elements
|
|
1455
1797
|
if (draftActivate && element.value) continue
|
|
1456
1798
|
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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 })
|
|
1799
|
+
// Make sure column is releveant & add it to output
|
|
1800
|
+
const isDraftInfo = DRAFT_ELEMENTS.has(el)
|
|
1801
|
+
const isGenerated = draftActivate && !!(element['@cds.on.insert'] || element['@cds.on.update'])
|
|
1802
|
+
const isLargeBinary = cds.env.fiori.move_media_data_in_db && draftActivate && element._type === 'cds.LargeBinary'
|
|
1803
|
+
const isSkippedType = element.isAssociation || isLargeBinary
|
|
1804
|
+
if (!isDraftInfo && !isGenerated && !isSkippedType && !element['@odata.draft.skip']) columns.push({ ref: [el] })
|
|
1805
|
+
|
|
1806
|
+
// Skip children where draft is explicitly disabled
|
|
1807
|
+
if (!element.isComposition || element._target['@odata.draft.enabled'] === false || element['@odata.draft.ignore'])
|
|
1808
|
+
continue // happens for texts if not @fiori.draft.enabled
|
|
1809
|
+
|
|
1810
|
+
// Make sure recursion depth is not exceeded
|
|
1811
|
+
const cacheKey = target.name + ':' + el
|
|
1812
|
+
let cache = recursion.get(cacheKey) ?? 0
|
|
1813
|
+
recursion.set(cacheKey, ++cache)
|
|
1814
|
+
if (cache >= MAX_RECURSION_DEPTH) continue
|
|
1815
|
+
|
|
1816
|
+
// Recurse
|
|
1817
|
+
const expandedColumns = expandStarStar(element._target, draftActivate, recursion)
|
|
1818
|
+
|
|
1819
|
+
// Unpack recursion result
|
|
1820
|
+
if (expandedColumns.length) columns.push({ ref: [el], expand: expandedColumns })
|
|
1499
1821
|
}
|
|
1822
|
+
|
|
1500
1823
|
return columns
|
|
1501
1824
|
}
|
|
1502
1825
|
|
|
@@ -1521,7 +1844,6 @@ async function beforeNew(req) {
|
|
|
1521
1844
|
}
|
|
1522
1845
|
_cleanseData(req.data, req.target)
|
|
1523
1846
|
}
|
|
1524
|
-
|
|
1525
1847
|
beforeNew._initial = true
|
|
1526
1848
|
|
|
1527
1849
|
async function onNew(req) {
|
|
@@ -1529,8 +1851,10 @@ async function onNew(req) {
|
|
|
1529
1851
|
|
|
1530
1852
|
if (req.target.actives['@Capabilities.InsertRestrictions.Insertable'] === false || req.target.actives['@readonly'])
|
|
1531
1853
|
req.reject({ code: 405, statusCode: 405 })
|
|
1854
|
+
|
|
1532
1855
|
req.query ??= INSERT.into(req.subject).entries(req.data || {}) //> support simple srv.send('NEW',entity,...)
|
|
1533
1856
|
const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
1857
|
+
|
|
1534
1858
|
// Only allowed for pseudo draft roots (entities with this action)
|
|
1535
1859
|
if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
|
|
1536
1860
|
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
|
|
@@ -1538,14 +1862,25 @@ async function onNew(req) {
|
|
|
1538
1862
|
_cleanUpOldDrafts(this, req.tenant)
|
|
1539
1863
|
|
|
1540
1864
|
let DraftUUID
|
|
1865
|
+
let DraftMessages = []
|
|
1866
|
+
let newDraftMessages = req.messages ?? req._messages ?? []
|
|
1541
1867
|
if (isDirectAccess) DraftUUID = cds.utils.uuid()
|
|
1542
1868
|
else {
|
|
1543
1869
|
const rootData = await SELECT.one(req.query.INSERT.into.ref[0].id)
|
|
1544
1870
|
.columns([
|
|
1545
1871
|
{ ref: ['DraftAdministrativeData_DraftUUID'] },
|
|
1546
|
-
{
|
|
1872
|
+
{
|
|
1873
|
+
ref: ['DraftAdministrativeData'],
|
|
1874
|
+
expand: [
|
|
1875
|
+
{ ref: ['InProcessByUser'] },
|
|
1876
|
+
...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
|
|
1877
|
+
? [{ ref: ['DraftMessages'] }]
|
|
1878
|
+
: [])
|
|
1879
|
+
]
|
|
1880
|
+
}
|
|
1547
1881
|
])
|
|
1548
1882
|
.where(req.query.INSERT.into.ref[0].where)
|
|
1883
|
+
|
|
1549
1884
|
if (!rootData) req.reject({ code: 404, statusCode: 404 })
|
|
1550
1885
|
if (!cds.context.user._is_privileged && rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1551
1886
|
req.reject({
|
|
@@ -1554,27 +1889,10 @@ async function onNew(req) {
|
|
|
1554
1889
|
message: 'DRAFT_LOCKED_BY_ANOTHER_USER',
|
|
1555
1890
|
args: [rootData.DraftAdministrativeData.InProcessByUser]
|
|
1556
1891
|
})
|
|
1892
|
+
|
|
1557
1893
|
DraftUUID = rootData.DraftAdministrativeData_DraftUUID
|
|
1894
|
+
DraftMessages = rootData.DraftAdministrativeData?.DraftMessages || []
|
|
1558
1895
|
}
|
|
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
1896
|
|
|
1579
1897
|
const _setDraftColumns = (obj, target) => {
|
|
1580
1898
|
const newObj = Object.assign({}, obj, { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false })
|
|
@@ -1594,15 +1912,49 @@ async function onNew(req) {
|
|
|
1594
1912
|
return newObj
|
|
1595
1913
|
}
|
|
1596
1914
|
|
|
1915
|
+
const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
|
|
1597
1916
|
const draftData = _setDraftColumns(req.query.INSERT.entries[0], req.target)
|
|
1598
|
-
|
|
1599
1917
|
const draftCQN = INSERT.into(req.subject).entries(draftData)
|
|
1600
1918
|
|
|
1601
|
-
await
|
|
1919
|
+
await this.run(draftCQN)
|
|
1602
1920
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1921
|
+
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
|
|
1922
|
+
newDraftMessages = _compileUpdatedDraftMessages(
|
|
1923
|
+
newDraftMessages,
|
|
1924
|
+
DraftMessages,
|
|
1925
|
+
draftData,
|
|
1926
|
+
req.query.INSERT.into.ref
|
|
1927
|
+
)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const adminDataCQN = isDirectAccess
|
|
1931
|
+
? INSERT.into('DRAFT.DraftAdministrativeData').entries({
|
|
1932
|
+
DraftUUID,
|
|
1933
|
+
CreationDateTime: timestamp,
|
|
1934
|
+
CreatedByUser: req.user.id,
|
|
1935
|
+
LastChangeDateTime: timestamp,
|
|
1936
|
+
LastChangedByUser: req.user.id,
|
|
1937
|
+
DraftIsCreatedByMe: true, // Dummy values
|
|
1938
|
+
DraftIsProcessedByMe: true, // Dummy values
|
|
1939
|
+
InProcessByUser: req.user.id,
|
|
1940
|
+
...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
|
|
1941
|
+
? { DraftMessages: newDraftMessages }
|
|
1942
|
+
: {})
|
|
1943
|
+
})
|
|
1944
|
+
: UPDATE('DRAFT.DraftAdministrativeData')
|
|
1945
|
+
.data({
|
|
1946
|
+
InProcessByUser: req.user.id,
|
|
1947
|
+
LastChangedByUser: req.user.id,
|
|
1948
|
+
LastChangeDateTime: timestamp,
|
|
1949
|
+
...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
|
|
1950
|
+
? { DraftMessages: newDraftMessages }
|
|
1951
|
+
: {})
|
|
1952
|
+
})
|
|
1953
|
+
.where({ DraftUUID })
|
|
1954
|
+
|
|
1955
|
+
await adminDataCQN
|
|
1956
|
+
|
|
1957
|
+
if (req.protocol?.match(/odata/)) req._.readAfterWrite = true
|
|
1606
1958
|
|
|
1607
1959
|
return { ...draftData, IsActiveEntity: false }
|
|
1608
1960
|
}
|
|
@@ -1612,8 +1964,8 @@ async function onEdit(req) {
|
|
|
1612
1964
|
|
|
1613
1965
|
req.query ??= SELECT.from(req.target, req.data).where({ IsActiveEntity: true }) //> support simple srv.send('EDIT',entity,...)
|
|
1614
1966
|
|
|
1615
|
-
//
|
|
1616
|
-
const draftParams = req.query[$draftParams] || { IsActiveEntity: true }
|
|
1967
|
+
// REVISIT: can draftParams in the edit case ever be undefined or other than IsActiveEntity=true ?
|
|
1968
|
+
const draftParams = req.query[$draftParams] || { IsActiveEntity: true }
|
|
1617
1969
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
|
|
1618
1970
|
req.reject({
|
|
1619
1971
|
code: 400,
|
|
@@ -1650,6 +2002,7 @@ async function onEdit(req) {
|
|
|
1650
2002
|
}
|
|
1651
2003
|
}
|
|
1652
2004
|
}
|
|
2005
|
+
|
|
1653
2006
|
_addDraftColumns(req.target, cols)
|
|
1654
2007
|
|
|
1655
2008
|
const draftsRef = _redirectRefToDrafts(req.query.SELECT.from.ref, this.model)
|
|
@@ -1698,6 +2051,7 @@ async function onEdit(req) {
|
|
|
1698
2051
|
})
|
|
1699
2052
|
return w
|
|
1700
2053
|
})()
|
|
2054
|
+
|
|
1701
2055
|
const activeLockCQN = SELECT.from(lockTarget, [1]).where(lockWhere).forUpdate({ wait: 0 })
|
|
1702
2056
|
activeLockCQN.SELECT.localized = false
|
|
1703
2057
|
activeLockCQN[$draftParams] = draftParams
|
|
@@ -1775,16 +2129,11 @@ async function onEdit(req) {
|
|
|
1775
2129
|
// change to db run
|
|
1776
2130
|
await INSERT.into(req.target.drafts).entries(res)
|
|
1777
2131
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
if (req._?.odataRes) {
|
|
1782
|
-
req._?.odataRes?.setStatusCode(201, { overwrite: true })
|
|
1783
|
-
} else if (req.res) {
|
|
2132
|
+
if (req.res) {
|
|
2133
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
2134
|
+
// REVISIT: needs reworking for new adapter, especially re $batch
|
|
1784
2135
|
req.res.status(201)
|
|
1785
|
-
}
|
|
1786
2136
|
|
|
1787
|
-
if (req.res) {
|
|
1788
2137
|
const read_result = await _readAfterDraftAction.bind(this)({
|
|
1789
2138
|
req,
|
|
1790
2139
|
payload: res,
|
|
@@ -1804,7 +2153,6 @@ async function onCancel(req) {
|
|
|
1804
2153
|
LOG.debug('delete draft')
|
|
1805
2154
|
|
|
1806
2155
|
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
2156
|
const draftParams = req.query[$draftParams] || { IsActiveEntity: false } // REVISIT: can draftParams in the cancel case ever be undefined or other than IsActiveEntity=false ?
|
|
1809
2157
|
|
|
1810
2158
|
const draftQuery = SELECT.one
|
|
@@ -1816,16 +2164,16 @@ async function onCancel(req) {
|
|
|
1816
2164
|
if (req._etagValidationClause && draftParams.IsActiveEntity === false) draftQuery.where(req._etagValidationClause)
|
|
1817
2165
|
// do not add InProcessByUser restriction
|
|
1818
2166
|
const draft = await draftQuery
|
|
1819
|
-
if (
|
|
2167
|
+
if (!draft) req.reject(req._etagValidationType ? 412 : 404)
|
|
1820
2168
|
if (draft) {
|
|
1821
2169
|
const processByUser = draft.DraftAdministrativeData?.InProcessByUser
|
|
1822
2170
|
if (!cds.context.user._is_privileged && processByUser && processByUser !== cds.context.user.id)
|
|
1823
2171
|
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [processByUser] })
|
|
1824
2172
|
}
|
|
2173
|
+
|
|
1825
2174
|
const draftDeleteQuery = DELETE.from({ ref: req.query.DELETE.from.ref }) // REVISIT: Isn't that == req.query ?
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
: [draftParams.IsActiveEntity === false ? this.run(draftDeleteQuery, req.data) : draftDeleteQuery]
|
|
2175
|
+
|
|
2176
|
+
const queries = !draft ? [] : [this.run(draftDeleteQuery, req.data)]
|
|
1829
2177
|
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
1830
2178
|
// only for draft root
|
|
1831
2179
|
queries.push(
|
|
@@ -1841,8 +2189,6 @@ async function onCancel(req) {
|
|
|
1841
2189
|
})
|
|
1842
2190
|
.where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
1843
2191
|
)
|
|
1844
|
-
if (draftParams.IsActiveEntity !== false && !req.target.isDraft)
|
|
1845
|
-
queries.push(this.run(DELETE.from({ ref: activeRef })))
|
|
1846
2192
|
await _promiseAll(queries)
|
|
1847
2193
|
return req.data
|
|
1848
2194
|
}
|
|
@@ -1862,7 +2208,7 @@ async function onPrepare(req) {
|
|
|
1862
2208
|
|
|
1863
2209
|
const draftQuery = SELECT.one
|
|
1864
2210
|
.from(req.target, d => {
|
|
1865
|
-
d`.*`, d.DraftAdministrativeData(a => a.InProcessByUser)
|
|
2211
|
+
;(d`.*`, d.DraftAdministrativeData(a => a.InProcessByUser))
|
|
1866
2212
|
})
|
|
1867
2213
|
.where(where)
|
|
1868
2214
|
draftQuery[$draftParams] = draftParams
|
|
@@ -1959,8 +2305,8 @@ module.exports = cds.service.impl(function () {
|
|
|
1959
2305
|
}
|
|
1960
2306
|
|
|
1961
2307
|
// Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
|
|
1962
|
-
this.on('NEW', '*', _wrapped(onNew, false))
|
|
1963
2308
|
this.prepend(s => s.before('NEW', '*', _wrapped(beforeNew, false)))
|
|
2309
|
+
this.on('NEW', '*', _wrapped(onNew, false))
|
|
1964
2310
|
this.on('EDIT', '*', _wrapped(onEdit, true))
|
|
1965
2311
|
this.on('CANCEL', '*', _wrapped(onCancel, false))
|
|
1966
2312
|
this.on('draftPrepare', '*', _wrapped(onPrepare, false))
|