@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.
- package/CHANGELOG.md +68 -0
- package/bin/deploy.js +29 -0
- package/bin/serve.js +1 -5
- package/lib/compile/etc/csv.js +11 -6
- package/lib/compile/for/lean_drafts.js +29 -7
- package/lib/compile/load.js +8 -5
- package/lib/compile/to/hdbtabledata.js +1 -1
- package/lib/dbs/cds-deploy.js +5 -34
- package/lib/env/cds-env.js +2 -1
- package/lib/env/cds-requires.js +4 -1
- package/lib/env/defaults.js +0 -11
- package/lib/env/schemas/cds-rc.js +218 -6
- package/lib/index.js +38 -38
- package/lib/log/cds-error.js +12 -11
- package/lib/log/format/json.js +1 -1
- package/lib/ql/SELECT.js +31 -0
- package/lib/ql/resolve.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/req/validate.js +17 -19
- package/lib/srv/cds.Service.js +18 -28
- package/lib/srv/middlewares/auth/ias-auth.js +29 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/srv/srv-models.js +1 -1
- package/lib/srv/srv-tx.js +2 -2
- package/lib/utils/cds-utils.js +35 -2
- package/lib/utils/csv-reader.js +1 -1
- package/lib/utils/inflect.js +2 -2
- package/lib/utils/tar.js +60 -23
- package/lib/utils/version.js +18 -0
- package/libx/_runtime/cds.js +1 -1
- package/libx/_runtime/common/aspects/any.js +1 -23
- package/libx/_runtime/common/generic/crud.js +1 -3
- package/libx/_runtime/common/generic/input.js +113 -52
- package/libx/_runtime/common/generic/sorting.js +1 -1
- package/libx/_runtime/common/generic/temporal.js +0 -6
- package/libx/_runtime/common/utils/draft.js +1 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
- package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/structured.js +2 -2
- package/libx/_runtime/common/utils/templateProcessor.js +0 -5
- package/libx/_runtime/common/utils/vcap.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +529 -143
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
- package/libx/_runtime/messaging/service.js +1 -1
- package/libx/_runtime/remote/utils/client.js +2 -1
- package/libx/common/assert/utils.js +2 -12
- package/libx/common/utils/streaming.js +4 -9
- package/libx/http/location.js +1 -0
- package/libx/odata/ODataAdapter.js +47 -43
- package/libx/odata/index.js +1 -1
- package/libx/odata/middleware/batch.js +6 -2
- package/libx/odata/middleware/create.js +1 -1
- package/libx/odata/middleware/error.js +27 -17
- package/libx/odata/middleware/operation.js +15 -21
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/parse/afterburner.js +22 -8
- package/libx/odata/parse/cqn2odata.js +16 -10
- package/libx/odata/parse/grammar.peggy +185 -134
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +1 -36
- 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 +7 -4
- package/libx/rest/RestAdapter.js +1 -2
- package/libx/rest/middleware/create.js +5 -2
- package/package.json +2 -2
- package/server.js +1 -1
- package/bin/deploy/to-hana.js +0 -1
- package/lib/utils/check-version.js +0 -9
- package/lib/utils/unit.js +0 -19
- package/libx/_runtime/cds-services/util/assert.js +0 -181
- package/libx/_runtime/types/api.js +0 -129
- 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.
|
|
27
|
+
timeout_ms = cds.utils.ms4(_default)
|
|
25
28
|
} else if (typeof timeout === 'string') {
|
|
26
|
-
timeout_ms = cds.utils.
|
|
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
|
-
|
|
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
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
527
|
-
const
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
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
|
|
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(
|
|
824
|
+
.columns(columns)
|
|
560
825
|
.columns([
|
|
561
|
-
'
|
|
562
|
-
'
|
|
563
|
-
{ ref: ['
|
|
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 (
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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: [
|
|
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
|
-
//
|
|
1836
|
+
// Skip calculated elements
|
|
1455
1837
|
if (draftActivate && element.value) continue
|
|
1456
1838
|
|
|
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 })
|
|
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
|
-
{
|
|
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
|
|
1959
|
+
await this.run(draftCQN)
|
|
1602
1960
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
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
|
-
//
|
|
1616
|
-
const draftParams = req.query[$draftParams] || { 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
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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 (
|
|
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
|
-
|
|
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))
|