@sap/cds 8.1.0 → 8.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 +60 -0
- package/app/index.css +3 -0
- package/app/index.js +50 -4
- package/bin/serve.js +1 -1
- package/lib/compile/cdsc.js +2 -2
- package/lib/compile/etc/_localized.js +1 -1
- package/lib/compile/for/lean_drafts.js +1 -0
- package/lib/compile/to/sql.js +2 -2
- package/lib/env/cds-requires.js +6 -0
- package/lib/env/defaults.js +14 -3
- package/lib/env/plugins.js +6 -22
- package/lib/index.js +3 -2
- package/lib/linked/classes.js +0 -14
- package/lib/linked/types.js +12 -0
- package/lib/linked/validate.js +3 -2
- package/lib/log/cds-log.js +3 -3
- package/lib/log/format/aspects/als.js +23 -29
- package/lib/log/format/aspects/cls.js +9 -0
- package/lib/log/format/json.js +42 -6
- package/lib/ql/Whereable.js +5 -1
- package/lib/req/context.js +1 -0
- package/lib/req/locale.js +1 -1
- package/lib/srv/cds-connect.js +33 -32
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/srv-tx.js +1 -0
- package/lib/utils/cds-utils.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +2 -31
- package/libx/_runtime/common/generic/auth/utils.js +2 -0
- package/libx/_runtime/common/generic/input.js +2 -11
- package/libx/_runtime/common/generic/put.js +1 -10
- package/libx/_runtime/common/utils/binary.js +1 -7
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +10 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +12 -1
- package/libx/_runtime/common/utils/template.js +26 -16
- package/libx/_runtime/common/utils/templateProcessor.js +8 -7
- package/libx/_runtime/common/utils/ucsn.js +2 -5
- package/libx/_runtime/db/expand/expandCQNToJoin.js +43 -2
- package/libx/_runtime/db/generic/input.js +1 -5
- package/libx/_runtime/fiori/lean-draft.js +287 -96
- package/libx/_runtime/messaging/event-broker.js +105 -40
- package/libx/_runtime/remote/Service.js +3 -1
- package/libx/_runtime/remote/utils/client.js +12 -4
- package/libx/_runtime/ucl/Service.js +16 -6
- package/libx/odata/middleware/batch.js +2 -2
- package/libx/odata/middleware/create.js +5 -0
- package/libx/odata/middleware/delete.js +5 -0
- package/libx/odata/middleware/error.js +1 -0
- package/libx/odata/middleware/operation.js +6 -0
- package/libx/odata/middleware/read.js +16 -11
- package/libx/odata/middleware/stream.js +4 -5
- package/libx/odata/middleware/update.js +9 -4
- package/libx/odata/parse/afterburner.js +3 -2
- package/libx/odata/parse/multipartToJson.js +1 -1
- package/libx/odata/utils/index.js +3 -3
- package/libx/odata/utils/postProcess.js +3 -25
- package/libx/rest/middleware/error.js +1 -0
- package/libx/rest/middleware/parse.js +1 -6
- package/package.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { Readable, PassThrough } = require('stream')
|
|
1
2
|
const cds = require('../cds'),
|
|
2
3
|
{ Object_keys } = cds.utils
|
|
3
4
|
|
|
@@ -241,6 +242,67 @@ const _cleanUpOldDrafts = (service, tenant) => {
|
|
|
241
242
|
lastCheckMap.set(tenant, Date.now())
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
const _hasStreaming = (cols, target, deep) => {
|
|
246
|
+
return cols?.some(col => {
|
|
247
|
+
const name = col.as || col.ref?.at(-1)
|
|
248
|
+
if (!target.elements[name]) return
|
|
249
|
+
return (
|
|
250
|
+
target.elements[name]._type === 'cds.LargeBinary' ||
|
|
251
|
+
(deep && col.expand && _hasStreaming(col.expand, target.elements[name]._target, deep))
|
|
252
|
+
)
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const _waitForReadable = readable => {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
readable.once('readable', resolve)
|
|
259
|
+
readable.once('error', reject)
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const _removeEmptyStreams = async result => {
|
|
264
|
+
if (!result) return
|
|
265
|
+
|
|
266
|
+
const res = Array.isArray(result) ? result : [result]
|
|
267
|
+
for (let r of res) {
|
|
268
|
+
for (let key in r) {
|
|
269
|
+
const el = r[key]
|
|
270
|
+
if (el instanceof Readable) {
|
|
271
|
+
// In case hana-client Readable may not be ready
|
|
272
|
+
if (cds.db?.constructor?.name === 'HANAService') await _waitForReadable(el)
|
|
273
|
+
const chunk0 = el.read()
|
|
274
|
+
if (chunk0 === null) delete r[key]
|
|
275
|
+
else el.unshift(chunk0)
|
|
276
|
+
} else if (typeof el === 'object') {
|
|
277
|
+
const res = Array.isArray(el) ? el : [el]
|
|
278
|
+
for (let r of res) {
|
|
279
|
+
await _removeEmptyStreams(r)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 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
|
|
287
|
+
const _replaceStreams = result => {
|
|
288
|
+
if (!result) return
|
|
289
|
+
|
|
290
|
+
const res = Array.isArray(result) ? result : [result]
|
|
291
|
+
for (let r of res) {
|
|
292
|
+
for (let key in r) {
|
|
293
|
+
const el = r[key]
|
|
294
|
+
if (el instanceof Readable) {
|
|
295
|
+
const stream = new Readable()
|
|
296
|
+
stream.push(null)
|
|
297
|
+
r[key] = stream
|
|
298
|
+
} else if (typeof el === 'object') {
|
|
299
|
+
const res = Array.isArray(el) ? el : [el]
|
|
300
|
+
res.forEach(_replaceStreams)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
244
306
|
const h = cds.ApplicationService.prototype.handle
|
|
245
307
|
|
|
246
308
|
cds.ApplicationService.prototype.handle = async function (req) {
|
|
@@ -333,78 +395,86 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
333
395
|
req.query = query
|
|
334
396
|
return handle(req)
|
|
335
397
|
}
|
|
336
|
-
const read =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
398
|
+
const read =
|
|
399
|
+
draftParams.IsActiveEntity === false &&
|
|
400
|
+
_hasStreaming(query.SELECT.columns, query._target) &&
|
|
401
|
+
!cds.env.features.binary_draft_compat
|
|
402
|
+
? Read.draftStream
|
|
403
|
+
: req.query._target.name.endsWith('.drafts')
|
|
404
|
+
? Read.ownDrafts
|
|
405
|
+
: draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
|
|
406
|
+
? Read.all
|
|
407
|
+
: draftParams.IsActiveEntity === true &&
|
|
408
|
+
draftParams.SiblingEntity_IsActiveEntity === null &&
|
|
409
|
+
(draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
|
|
410
|
+
draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
|
|
411
|
+
? Read.lockedByAnotherUser
|
|
412
|
+
: draftParams.IsActiveEntity === true &&
|
|
413
|
+
draftParams.SiblingEntity_IsActiveEntity === null &&
|
|
414
|
+
draftParams.DraftAdministrativeData_InProcessByUser === ''
|
|
415
|
+
? Read.unsavedChangesByAnotherUser
|
|
416
|
+
: draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
|
|
417
|
+
? Read.unchanged
|
|
418
|
+
: draftParams.IsActiveEntity === true
|
|
419
|
+
? Read.onlyActives
|
|
420
|
+
: draftParams.IsActiveEntity === false
|
|
421
|
+
? Read.ownDrafts
|
|
422
|
+
: Read.onlyActives
|
|
356
423
|
const result = await read(run, query)
|
|
357
424
|
return result
|
|
358
425
|
}
|
|
359
426
|
|
|
360
427
|
if (req.event === 'draftEdit') req.event = 'EDIT'
|
|
428
|
+
if (req.event === 'draftPrepare' && draftParams.IsActiveEntity) req.reject({ code: 400, statusCode: 400 })
|
|
361
429
|
|
|
430
|
+
// Create active instance of draft-enabled entity
|
|
431
|
+
// Careful: New OData adapter only sets `NEW` for drafts... how to distinguish programmatic modifications?
|
|
362
432
|
if (
|
|
363
|
-
req.event === 'NEW' ||
|
|
364
|
-
req.event === '
|
|
365
|
-
req.event === 'draftPrepare' ||
|
|
366
|
-
(req.event === 'CREATE' && req.target._isDraftEnabled)
|
|
433
|
+
(req.event === 'NEW' && req.data.IsActiveEntity === true) || // old OData adapter changes CREATE to NEW also for actives
|
|
434
|
+
(req.event === 'CREATE' && req.target.drafts && req.data?.IsActiveEntity !== false && !req.target.isDraft)
|
|
367
435
|
) {
|
|
368
|
-
if (req.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
436
|
+
if (req.protocol === 'odata' && !cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass'])
|
|
437
|
+
return reject_bypassed_draft(req)
|
|
438
|
+
const containsDraftRoot =
|
|
439
|
+
this.model.definitions[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
|
|
440
|
+
'@Common.DraftRoot.ActivationAction'
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
if (!containsDraftRoot) req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
|
|
444
|
+
|
|
445
|
+
const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
446
|
+
const data = Object.assign({}, req.data) // IsActiveEntity is not enumerable
|
|
447
|
+
const draftsRootRef =
|
|
448
|
+
typeof query.INSERT.into === 'string'
|
|
449
|
+
? [req.target.drafts.name]
|
|
450
|
+
: _redirectRefToDrafts([query.INSERT.into.ref[0]], this.model)
|
|
451
|
+
let rootHasDraft
|
|
452
|
+
|
|
453
|
+
// children: check root entity has no draft
|
|
454
|
+
if (!isDirectAccess) {
|
|
455
|
+
rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef })
|
|
456
|
+
}
|
|
390
457
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
458
|
+
// direct access and req.data contains keys: check if root has no draft with that keys
|
|
459
|
+
if (isDirectAccess && entity_keys(query._target).every(k => k in data)) {
|
|
460
|
+
const keyData = entity_keys(query._target).reduce((res, k) => {
|
|
461
|
+
res[k] = req.data[k]
|
|
462
|
+
return res
|
|
463
|
+
}, {})
|
|
464
|
+
rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef }).where(keyData)
|
|
465
|
+
}
|
|
399
466
|
|
|
400
|
-
|
|
467
|
+
if (rootHasDraft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
401
468
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
469
|
+
const cqn = INSERT.into(query.INSERT.into).entries(data)
|
|
470
|
+
await run(cqn, { event: 'CREATE' })
|
|
471
|
+
const result = { ...data, IsActiveEntity: true }
|
|
472
|
+
req.data = result //> make keys available via req.data (as with normal crud)
|
|
473
|
+
return result
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// It needs to be redirected to drafts
|
|
477
|
+
if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
|
|
408
478
|
req.target = req.target.drafts
|
|
409
479
|
|
|
410
480
|
if (query.INSERT?.into) {
|
|
@@ -418,6 +488,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
418
488
|
|
|
419
489
|
const _req = _newReq(req, query, draftParams, { event: req.event })
|
|
420
490
|
|
|
491
|
+
// Do not allow to create active instances via drafts
|
|
421
492
|
if (
|
|
422
493
|
(req.event === 'NEW' || req.event === 'CREATE') &&
|
|
423
494
|
draftParams.IsActiveEntity === false &&
|
|
@@ -431,7 +502,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
431
502
|
return result
|
|
432
503
|
}
|
|
433
504
|
|
|
434
|
-
|
|
505
|
+
// Delete active instance of draft-enabled entity
|
|
506
|
+
if (req.target.drafts && !req.target.isDraft && req.event === 'DELETE' && draftParams.IsActiveEntity !== false) {
|
|
435
507
|
const draftsRef = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
|
|
436
508
|
const draftQuery = SELECT.one.from({ ref: draftsRef }).columns([
|
|
437
509
|
{ ref: ['DraftAdministrativeData_DraftUUID'] },
|
|
@@ -440,6 +512,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
440
512
|
expand: [_inProcessByUserXpr(_lock.shiftedNow)]
|
|
441
513
|
}
|
|
442
514
|
])
|
|
515
|
+
if (query.DELETE.where) draftQuery.where(query.DELETE.where)
|
|
443
516
|
|
|
444
517
|
// Deletion of active instance outside draft tree, no need to check for draft
|
|
445
518
|
if (!draftQuery.target?.isDraft) {
|
|
@@ -462,7 +535,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
462
535
|
|
|
463
536
|
// It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
|
|
464
537
|
// be implemented in expand implementation.
|
|
465
|
-
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity
|
|
538
|
+
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity === true) {
|
|
466
539
|
req.reject({
|
|
467
540
|
code: 400,
|
|
468
541
|
statusCode: 400,
|
|
@@ -480,16 +553,16 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
480
553
|
// Use `run` (since also etags might need to be checked)
|
|
481
554
|
// REVISIT: Find a better approach (`etag` as part of CQN?)
|
|
482
555
|
const draftRef = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
)
|
|
556
|
+
const draftQuery = SELECT.one
|
|
557
|
+
.from({ ref: draftRef })
|
|
558
|
+
.columns(cols)
|
|
559
|
+
.columns([
|
|
560
|
+
'HasActiveEntity',
|
|
561
|
+
'DraftAdministrativeData_DraftUUID',
|
|
562
|
+
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
563
|
+
])
|
|
564
|
+
.where(query.SELECT.where)
|
|
565
|
+
const res = await run(draftQuery)
|
|
493
566
|
if (!res)
|
|
494
567
|
req.reject(_etagValidationType ? { code: 412, statusCode: 412 } : { code: 'DRAFT_NOT_EXISTING', statusCode: 404 })
|
|
495
568
|
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
@@ -507,15 +580,19 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
507
580
|
const HasActiveEntity = res.HasActiveEntity
|
|
508
581
|
delete res.HasActiveEntity
|
|
509
582
|
|
|
583
|
+
if (_hasStreaming(draftQuery.SELECT.columns, draftQuery._target, true) && !cds.env.features.binary_draft_compat)
|
|
584
|
+
await _removeEmptyStreams(res)
|
|
585
|
+
|
|
510
586
|
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
587
|
+
const activesRef = _redirectRefToActives(query.SELECT.from.ref, this.model)
|
|
511
588
|
const result = await run(
|
|
512
589
|
HasActiveEntity
|
|
513
|
-
? UPDATE({ ref:
|
|
514
|
-
: INSERT.into({ ref:
|
|
590
|
+
? UPDATE({ ref: activesRef }).data(res).where(query.SELECT.where)
|
|
591
|
+
: INSERT.into({ ref: activesRef }).entries(res),
|
|
515
592
|
{ headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
|
|
516
593
|
)
|
|
517
594
|
await _promiseAll([
|
|
518
|
-
DELETE.from({ ref: draftRef }),
|
|
595
|
+
DELETE.from({ ref: draftRef }).where(query.SELECT.where),
|
|
519
596
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
520
597
|
])
|
|
521
598
|
|
|
@@ -536,7 +613,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
536
613
|
payload: res,
|
|
537
614
|
action: 'draftActivate'
|
|
538
615
|
})
|
|
539
|
-
req.res
|
|
616
|
+
req.res?.set(
|
|
540
617
|
'location',
|
|
541
618
|
'../' + calculateLocationHeader(req.target, this, read_result || { ...res, IsActiveEntity: true })
|
|
542
619
|
)
|
|
@@ -563,8 +640,6 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
563
640
|
}
|
|
564
641
|
|
|
565
642
|
if (req.event === 'PATCH' || (req.event === 'UPDATE' && req.target.drafts)) {
|
|
566
|
-
if (!('IsActiveEntity' in draftParams)) req.reject({ code: 501, statusCode: 501 })
|
|
567
|
-
|
|
568
643
|
if (draftParams.IsActiveEntity === false) {
|
|
569
644
|
LOG.debug('patch draft')
|
|
570
645
|
|
|
@@ -600,16 +675,19 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
600
675
|
|
|
601
676
|
LOG.debug('patch active')
|
|
602
677
|
|
|
603
|
-
if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass'])
|
|
678
|
+
if (req.protocol === 'odata' && !cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass'])
|
|
679
|
+
return reject_bypassed_draft(req)
|
|
604
680
|
|
|
605
681
|
const entityRef = query.UPDATE.entity.ref
|
|
606
682
|
|
|
607
|
-
if (!this.model.definitions[entityRef[0].id]['@Common.DraftRoot.ActivationAction']) {
|
|
683
|
+
if (!this.model.definitions[entityRef[0].id || entityRef[0]]['@Common.DraftRoot.ActivationAction']) {
|
|
608
684
|
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
|
|
609
685
|
}
|
|
610
686
|
|
|
611
687
|
const draftsRef = _redirectRefToDrafts(entityRef, this.model)
|
|
612
|
-
const
|
|
688
|
+
const draftsQuery = SELECT.one([1]).from({ ref: [draftsRef[0]] })
|
|
689
|
+
if (query.UPDATE.where) draftsQuery.where(query.UPDATE.where)
|
|
690
|
+
const hasDraft = !!(await draftsQuery)
|
|
613
691
|
if (hasDraft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
614
692
|
|
|
615
693
|
await run(query)
|
|
@@ -651,6 +729,52 @@ const _requested = (result, query) => {
|
|
|
651
729
|
return result
|
|
652
730
|
}
|
|
653
731
|
|
|
732
|
+
const _readDraftStream = (draftStream, activeCQN, property) =>
|
|
733
|
+
Readable.from(
|
|
734
|
+
(async function* () {
|
|
735
|
+
let isActive = true
|
|
736
|
+
this._stream = draftStream
|
|
737
|
+
for await (const chunk of draftStream) {
|
|
738
|
+
isActive = false
|
|
739
|
+
yield chunk
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (isActive) {
|
|
743
|
+
const active = (await activeCQN)?.[property]
|
|
744
|
+
if (active) {
|
|
745
|
+
for await (const chunk of active) {
|
|
746
|
+
yield chunk
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
})()
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
// REVISIT: HanaLobStream of @sap/hana-client cannot read chunks with "for await" - hangs
|
|
754
|
+
const _readDraftStreamHanaClient = async (draftStream, activeCQN, property) =>
|
|
755
|
+
Readable.from(
|
|
756
|
+
(async function* () {
|
|
757
|
+
let isActive = true
|
|
758
|
+
const pth = new PassThrough()
|
|
759
|
+
draftStream.pipe(pth)
|
|
760
|
+
for await (const chunk of pth) {
|
|
761
|
+
isActive = false
|
|
762
|
+
yield chunk
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (isActive) {
|
|
766
|
+
const active = (await activeCQN)?.[property]
|
|
767
|
+
if (active) {
|
|
768
|
+
const pth = new PassThrough()
|
|
769
|
+
active.pipe(pth)
|
|
770
|
+
for await (const chunk of pth) {
|
|
771
|
+
yield chunk
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
})()
|
|
776
|
+
)
|
|
777
|
+
|
|
654
778
|
const Read = {
|
|
655
779
|
onlyActives: async function (run, query, { ignoreDrafts } = {}) {
|
|
656
780
|
LOG.debug('List Editing Status: Only Active')
|
|
@@ -737,6 +861,7 @@ const Read = {
|
|
|
737
861
|
'=',
|
|
738
862
|
cds.context.user.id
|
|
739
863
|
)
|
|
864
|
+
|
|
740
865
|
const drafts = await run(draftsQuery)
|
|
741
866
|
Read.merge(query._target, drafts, [], row => {
|
|
742
867
|
Object.assign(row, {
|
|
@@ -862,8 +987,11 @@ const Read = {
|
|
|
862
987
|
const locale = cds.context.locale.replaceAll('_', '-')
|
|
863
988
|
const collatorMap = new Map()
|
|
864
989
|
const elementNamesToSort = orderByExpr.map(orderByExp => orderByExp.ref.join('_'))
|
|
990
|
+
|
|
865
991
|
for (const elementName of elementNamesToSort) {
|
|
866
|
-
const element = queryElements[elementName]
|
|
992
|
+
const element = queryElements[elementName] ?? query._target.elements[elementName] // The latter is needed for CDS orderBy statements
|
|
993
|
+
if (!element) continue
|
|
994
|
+
|
|
867
995
|
let collatorOptions
|
|
868
996
|
|
|
869
997
|
switch (element.type) {
|
|
@@ -994,6 +1122,23 @@ const Read = {
|
|
|
994
1122
|
return drafts
|
|
995
1123
|
},
|
|
996
1124
|
|
|
1125
|
+
draftStream: async (run, query) => {
|
|
1126
|
+
// read from draft
|
|
1127
|
+
const result = await Read.ownDrafts(run, query)
|
|
1128
|
+
if (!Array.isArray(result)) {
|
|
1129
|
+
for (let key in result) {
|
|
1130
|
+
if (result[key] instanceof Readable) {
|
|
1131
|
+
result[key] =
|
|
1132
|
+
result[key].constructor.name === 'HanaLobStream'
|
|
1133
|
+
? await _readDraftStreamHanaClient(result[key], query, key)
|
|
1134
|
+
: _readDraftStream(result[key], query, key)
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return result
|
|
1140
|
+
},
|
|
1141
|
+
|
|
997
1142
|
_makeArray: data => (Array.isArray(data) ? data : data ? [data] : []),
|
|
998
1143
|
|
|
999
1144
|
_index: (target, data) => {
|
|
@@ -1081,14 +1226,23 @@ function _cleansed(query, model) {
|
|
|
1081
1226
|
}
|
|
1082
1227
|
cds.infer(draftsQuery, model.definitions)
|
|
1083
1228
|
// draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
|
|
1084
|
-
if (query.SELECT.columns && query._target.drafts)
|
|
1085
|
-
|
|
1229
|
+
if (query.SELECT.columns && query._target.drafts) {
|
|
1230
|
+
if (draftsQuery._target.isDraft)
|
|
1231
|
+
draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
|
|
1232
|
+
else draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, DRAFT_ELEMENTS, draft)
|
|
1233
|
+
}
|
|
1086
1234
|
|
|
1087
|
-
if (query.SELECT.where && query._target.drafts)
|
|
1088
|
-
|
|
1235
|
+
if (query.SELECT.where && query._target.drafts) {
|
|
1236
|
+
if (draftsQuery._target.isDraft)
|
|
1237
|
+
draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
|
|
1238
|
+
else draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS)
|
|
1239
|
+
}
|
|
1089
1240
|
|
|
1090
|
-
if (query.SELECT.orderBy && query._target.drafts)
|
|
1091
|
-
|
|
1241
|
+
if (query.SELECT.orderBy && query._target.drafts) {
|
|
1242
|
+
if (draftsQuery._target.isDraft)
|
|
1243
|
+
draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
|
|
1244
|
+
else draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, DRAFT_ELEMENTS)
|
|
1245
|
+
}
|
|
1092
1246
|
|
|
1093
1247
|
if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
|
|
1094
1248
|
draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
|
|
@@ -1257,9 +1411,39 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
|
|
|
1257
1411
|
const columns = []
|
|
1258
1412
|
for (const el in target.elements) {
|
|
1259
1413
|
const element = target.elements[el]
|
|
1414
|
+
|
|
1415
|
+
// no need to read calculated elements
|
|
1416
|
+
if (draftActivate && element.value) continue
|
|
1417
|
+
|
|
1418
|
+
// REVISIT: This does not work with old HANA db layer.
|
|
1419
|
+
// Use it after removing old db layer.
|
|
1420
|
+
/*
|
|
1421
|
+
if (element._type === 'cds.LargeBinary') {
|
|
1422
|
+
if (!draftActivate) {
|
|
1423
|
+
columns.push({
|
|
1424
|
+
xpr: [
|
|
1425
|
+
'case',
|
|
1426
|
+
'when',
|
|
1427
|
+
{ ref: [el] },
|
|
1428
|
+
'IS',
|
|
1429
|
+
'NULL',
|
|
1430
|
+
'then',
|
|
1431
|
+
{ val: null },
|
|
1432
|
+
'else',
|
|
1433
|
+
{ val: '' },
|
|
1434
|
+
'end'
|
|
1435
|
+
],
|
|
1436
|
+
as: el,
|
|
1437
|
+
// cast: 'cds.LargeBinary' <-- This should be fixed for new HANA db layer
|
|
1438
|
+
})
|
|
1439
|
+
continue
|
|
1440
|
+
}
|
|
1441
|
+
}*/
|
|
1442
|
+
|
|
1260
1443
|
const skip_managed = draftActivate && (element['@cds.on.insert'] || element['@cds.on.update'])
|
|
1261
1444
|
if (!skip_managed && !element.isAssociation && !DRAFT_ELEMENTS.has(el) && !element['@odata.draft.skip'])
|
|
1262
1445
|
columns.push({ ref: [el] })
|
|
1446
|
+
|
|
1263
1447
|
if (!element.isComposition || element._target['@odata.draft.enabled'] === false) continue // happens for texts if not @fiori.draft.enabled
|
|
1264
1448
|
const _key = target.name + ':' + el
|
|
1265
1449
|
let cache = recursion.get(_key)
|
|
@@ -1388,7 +1572,7 @@ async function onEdit(req) {
|
|
|
1388
1572
|
const DraftUUID = cds.utils.uuid()
|
|
1389
1573
|
|
|
1390
1574
|
// REVISIT: Later optimization if datasource === db: INSERT FROM SELECT
|
|
1391
|
-
const cols = expandStarStar(req.target)
|
|
1575
|
+
const cols = expandStarStar(req.target, false)
|
|
1392
1576
|
const _addDraftColumns = (target, columns) => {
|
|
1393
1577
|
if (target.drafts) {
|
|
1394
1578
|
columns.push({ val: true, as: 'HasActiveEntity' })
|
|
@@ -1450,11 +1634,13 @@ async function onEdit(req) {
|
|
|
1450
1634
|
return w
|
|
1451
1635
|
})()
|
|
1452
1636
|
const activeLockCQN = SELECT.from(lockTarget, [1]).where(lockWhere).forUpdate({ wait: 0 })
|
|
1637
|
+
activeLockCQN.SELECT.localized = false
|
|
1453
1638
|
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1454
1639
|
|
|
1455
1640
|
try {
|
|
1456
1641
|
await this.run(activeLockCQN)
|
|
1457
|
-
} catch {
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
LOG._debug && LOG.debug('Failed to acquire database lock:', error)
|
|
1458
1644
|
const draft = await this.run(existingDraft)
|
|
1459
1645
|
if (draft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
1460
1646
|
req.reject({ code: 409, statusCode: 409, message: 'ENTITY_LOCKED' })
|
|
@@ -1468,6 +1654,7 @@ async function onEdit(req) {
|
|
|
1468
1654
|
;[res, draft] = await _promiseAll(cqns)
|
|
1469
1655
|
} else {
|
|
1470
1656
|
const activeLockCQN = SELECT.from({ ref: req.query.SELECT.from.ref }, [1]).forUpdate({ wait: 0 })
|
|
1657
|
+
activeLockCQN.SELECT.localized = false
|
|
1471
1658
|
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1472
1659
|
|
|
1473
1660
|
// Locking the underlying database table is effective only when the database is not
|
|
@@ -1475,7 +1662,9 @@ async function onEdit(req) {
|
|
|
1475
1662
|
// a separate system.
|
|
1476
1663
|
try {
|
|
1477
1664
|
await activeLockCQN
|
|
1478
|
-
} catch {
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
LOG._debug && LOG.debug('Failed to acquire database lock:', error)
|
|
1667
|
+
}
|
|
1479
1668
|
|
|
1480
1669
|
;[res, draft] = await _promiseAll([
|
|
1481
1670
|
// REVISIT: inofficial compat flag just in case it breaks something -> do not document
|
|
@@ -1499,6 +1688,8 @@ async function onEdit(req) {
|
|
|
1499
1688
|
])
|
|
1500
1689
|
}
|
|
1501
1690
|
|
|
1691
|
+
if (!cds.env.features.stream_compat && !cds.env.features.binary_draft_compat) _replaceStreams(res)
|
|
1692
|
+
|
|
1502
1693
|
const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
|
|
1503
1694
|
await INSERT.into('DRAFT.DraftAdministrativeData').entries({
|
|
1504
1695
|
DraftUUID,
|
|
@@ -1630,12 +1821,12 @@ const _readAfterDraftAction = async function ({ req, payload, action }) {
|
|
|
1630
1821
|
|
|
1631
1822
|
// read after write with query options
|
|
1632
1823
|
const keys = {}
|
|
1633
|
-
entity.keys
|
|
1634
|
-
if (key.name === 'IsActiveEntity' || key.isAssociation || key.virtual)
|
|
1824
|
+
for (let key of entity.keys) {
|
|
1825
|
+
if (key.name === 'IsActiveEntity' || key.isAssociation || key.virtual) continue
|
|
1635
1826
|
keys[key.name] = payload[key.name]
|
|
1636
|
-
}
|
|
1827
|
+
}
|
|
1637
1828
|
const read = SELECT.one.from(entity, keys)
|
|
1638
|
-
if (req.req
|
|
1829
|
+
if (req.req?.query.$select || req.req?.query.$expand) {
|
|
1639
1830
|
const queryOptions = []
|
|
1640
1831
|
if (req.req.query.$select) queryOptions.push(`$select=${req.req.query.$select}`)
|
|
1641
1832
|
if (req.req.query.$expand) queryOptions.push(`$expand=${req.req.query.$expand}`)
|