@sap/cds 8.1.1 → 8.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -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/linked/classes.js +0 -14
- package/lib/linked/types.js +12 -0
- package/lib/linked/validate.js +13 -8
- 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/srv/cds-connect.js +33 -32
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/middlewares/cds-context.js +2 -1
- package/lib/utils/cds-utils.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +1 -1
- 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/resolveView.js +2 -2
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +19 -6
- 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 +10 -0
- package/libx/_runtime/db/generic/input.js +1 -5
- package/libx/_runtime/fiori/lean-draft.js +272 -90
- package/libx/_runtime/messaging/event-broker.js +105 -40
- 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/read.js +6 -10
- package/libx/odata/middleware/stream.js +4 -5
- package/libx/odata/parse/afterburner.js +3 -2
- package/libx/odata/parse/multipartToJson.js +3 -1
- package/libx/odata/utils/index.js +3 -3
- package/libx/odata/utils/postProcess.js +3 -25
- 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) => {
|
|
@@ -1266,9 +1411,39 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
|
|
|
1266
1411
|
const columns = []
|
|
1267
1412
|
for (const el in target.elements) {
|
|
1268
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
|
+
|
|
1269
1443
|
const skip_managed = draftActivate && (element['@cds.on.insert'] || element['@cds.on.update'])
|
|
1270
1444
|
if (!skip_managed && !element.isAssociation && !DRAFT_ELEMENTS.has(el) && !element['@odata.draft.skip'])
|
|
1271
1445
|
columns.push({ ref: [el] })
|
|
1446
|
+
|
|
1272
1447
|
if (!element.isComposition || element._target['@odata.draft.enabled'] === false) continue // happens for texts if not @fiori.draft.enabled
|
|
1273
1448
|
const _key = target.name + ':' + el
|
|
1274
1449
|
let cache = recursion.get(_key)
|
|
@@ -1397,7 +1572,7 @@ async function onEdit(req) {
|
|
|
1397
1572
|
const DraftUUID = cds.utils.uuid()
|
|
1398
1573
|
|
|
1399
1574
|
// REVISIT: Later optimization if datasource === db: INSERT FROM SELECT
|
|
1400
|
-
const cols = expandStarStar(req.target)
|
|
1575
|
+
const cols = expandStarStar(req.target, false)
|
|
1401
1576
|
const _addDraftColumns = (target, columns) => {
|
|
1402
1577
|
if (target.drafts) {
|
|
1403
1578
|
columns.push({ val: true, as: 'HasActiveEntity' })
|
|
@@ -1459,11 +1634,13 @@ async function onEdit(req) {
|
|
|
1459
1634
|
return w
|
|
1460
1635
|
})()
|
|
1461
1636
|
const activeLockCQN = SELECT.from(lockTarget, [1]).where(lockWhere).forUpdate({ wait: 0 })
|
|
1637
|
+
activeLockCQN.SELECT.localized = false
|
|
1462
1638
|
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1463
1639
|
|
|
1464
1640
|
try {
|
|
1465
1641
|
await this.run(activeLockCQN)
|
|
1466
|
-
} catch {
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
LOG._debug && LOG.debug('Failed to acquire database lock:', error)
|
|
1467
1644
|
const draft = await this.run(existingDraft)
|
|
1468
1645
|
if (draft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
1469
1646
|
req.reject({ code: 409, statusCode: 409, message: 'ENTITY_LOCKED' })
|
|
@@ -1477,6 +1654,7 @@ async function onEdit(req) {
|
|
|
1477
1654
|
;[res, draft] = await _promiseAll(cqns)
|
|
1478
1655
|
} else {
|
|
1479
1656
|
const activeLockCQN = SELECT.from({ ref: req.query.SELECT.from.ref }, [1]).forUpdate({ wait: 0 })
|
|
1657
|
+
activeLockCQN.SELECT.localized = false
|
|
1480
1658
|
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1481
1659
|
|
|
1482
1660
|
// Locking the underlying database table is effective only when the database is not
|
|
@@ -1484,7 +1662,9 @@ async function onEdit(req) {
|
|
|
1484
1662
|
// a separate system.
|
|
1485
1663
|
try {
|
|
1486
1664
|
await activeLockCQN
|
|
1487
|
-
} catch {
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
LOG._debug && LOG.debug('Failed to acquire database lock:', error)
|
|
1667
|
+
}
|
|
1488
1668
|
|
|
1489
1669
|
;[res, draft] = await _promiseAll([
|
|
1490
1670
|
// REVISIT: inofficial compat flag just in case it breaks something -> do not document
|
|
@@ -1508,6 +1688,8 @@ async function onEdit(req) {
|
|
|
1508
1688
|
])
|
|
1509
1689
|
}
|
|
1510
1690
|
|
|
1691
|
+
if (!cds.env.features.stream_compat && !cds.env.features.binary_draft_compat) _replaceStreams(res)
|
|
1692
|
+
|
|
1511
1693
|
const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
|
|
1512
1694
|
await INSERT.into('DRAFT.DraftAdministrativeData').entries({
|
|
1513
1695
|
DraftUUID,
|
|
@@ -1639,12 +1821,12 @@ const _readAfterDraftAction = async function ({ req, payload, action }) {
|
|
|
1639
1821
|
|
|
1640
1822
|
// read after write with query options
|
|
1641
1823
|
const keys = {}
|
|
1642
|
-
entity.keys
|
|
1643
|
-
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
|
|
1644
1826
|
keys[key.name] = payload[key.name]
|
|
1645
|
-
}
|
|
1827
|
+
}
|
|
1646
1828
|
const read = SELECT.one.from(entity, keys)
|
|
1647
|
-
if (req.req
|
|
1829
|
+
if (req.req?.query.$select || req.req?.query.$expand) {
|
|
1648
1830
|
const queryOptions = []
|
|
1649
1831
|
if (req.req.query.$select) queryOptions.push(`$select=${req.req.query.$select}`)
|
|
1650
1832
|
if (req.req.query.$expand) queryOptions.push(`$expand=${req.req.query.$expand}`)
|