@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/app/index.css +3 -0
  3. package/app/index.js +50 -4
  4. package/bin/serve.js +1 -1
  5. package/lib/compile/cdsc.js +2 -2
  6. package/lib/compile/etc/_localized.js +1 -1
  7. package/lib/compile/for/lean_drafts.js +1 -0
  8. package/lib/compile/to/sql.js +2 -2
  9. package/lib/env/cds-requires.js +6 -0
  10. package/lib/env/defaults.js +14 -3
  11. package/lib/env/plugins.js +6 -22
  12. package/lib/linked/classes.js +0 -14
  13. package/lib/linked/types.js +12 -0
  14. package/lib/linked/validate.js +13 -8
  15. package/lib/log/cds-log.js +3 -3
  16. package/lib/log/format/aspects/als.js +23 -29
  17. package/lib/log/format/aspects/cls.js +9 -0
  18. package/lib/log/format/json.js +42 -6
  19. package/lib/ql/Whereable.js +5 -1
  20. package/lib/srv/cds-connect.js +33 -32
  21. package/lib/srv/cds-serve.js +2 -1
  22. package/lib/srv/middlewares/cds-context.js +2 -1
  23. package/lib/utils/cds-utils.js +4 -2
  24. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +1 -1
  25. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -5
  26. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +2 -31
  27. package/libx/_runtime/common/generic/auth/utils.js +2 -0
  28. package/libx/_runtime/common/generic/input.js +2 -11
  29. package/libx/_runtime/common/generic/put.js +1 -10
  30. package/libx/_runtime/common/utils/binary.js +1 -7
  31. package/libx/_runtime/common/utils/resolveView.js +2 -2
  32. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  33. package/libx/_runtime/common/utils/streamProp.js +19 -6
  34. package/libx/_runtime/common/utils/template.js +26 -16
  35. package/libx/_runtime/common/utils/templateProcessor.js +8 -7
  36. package/libx/_runtime/common/utils/ucsn.js +2 -5
  37. package/libx/_runtime/db/expand/expandCQNToJoin.js +10 -0
  38. package/libx/_runtime/db/generic/input.js +1 -5
  39. package/libx/_runtime/fiori/lean-draft.js +272 -90
  40. package/libx/_runtime/messaging/event-broker.js +105 -40
  41. package/libx/_runtime/remote/utils/client.js +12 -4
  42. package/libx/_runtime/ucl/Service.js +16 -6
  43. package/libx/odata/middleware/batch.js +2 -2
  44. package/libx/odata/middleware/read.js +6 -10
  45. package/libx/odata/middleware/stream.js +4 -5
  46. package/libx/odata/parse/afterburner.js +3 -2
  47. package/libx/odata/parse/multipartToJson.js +3 -1
  48. package/libx/odata/utils/index.js +3 -3
  49. package/libx/odata/utils/postProcess.js +3 -25
  50. package/libx/rest/middleware/parse.js +1 -6
  51. 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 = req.query._target.name.endsWith('.drafts')
337
- ? Read.ownDrafts
338
- : draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
339
- ? Read.all
340
- : draftParams.IsActiveEntity === true &&
341
- draftParams.SiblingEntity_IsActiveEntity === null &&
342
- (draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
343
- draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
344
- ? Read.lockedByAnotherUser
345
- : draftParams.IsActiveEntity === true &&
346
- draftParams.SiblingEntity_IsActiveEntity === null &&
347
- draftParams.DraftAdministrativeData_InProcessByUser === ''
348
- ? Read.unsavedChangesByAnotherUser
349
- : draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
350
- ? Read.unchanged
351
- : draftParams.IsActiveEntity === true
352
- ? Read.onlyActives
353
- : draftParams.IsActiveEntity === false
354
- ? Read.ownDrafts
355
- : Read.onlyActives
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 === 'CANCEL' ||
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.event === 'draftPrepare' && draftParams.IsActiveEntity) req.reject({ code: 400, statusCode: 400 })
369
- if ((req.event === 'NEW' || req.event === 'CREATE') && req.data?.IsActiveEntity === true) {
370
- if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass']) return reject_bypassed_draft(req)
371
- const containsDraftRoot =
372
- this.model.definitions[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
373
- '@Common.DraftRoot.ActivationAction'
374
- ]
375
-
376
- if (!containsDraftRoot) req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
377
-
378
- const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
379
- const data = Object.assign({}, req.data) // IsActiveEntity is not enumerable
380
- const draftsRootRef =
381
- typeof query.INSERT.into === 'string'
382
- ? [req.target.drafts.name]
383
- : _redirectRefToDrafts([query.INSERT.into.ref[0]], this.model)
384
- let rootHasDraft
385
-
386
- // children: check root entity has no draft
387
- if (!isDirectAccess) {
388
- rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef })
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
- // direct access and req.data contains keys: check if root has no draft with that keys
392
- if (isDirectAccess && entity_keys(query._target).every(k => k in data)) {
393
- const keyData = entity_keys(query._target).reduce((res, k) => {
394
- res[k] = req.data[k]
395
- return res
396
- }, {})
397
- rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef }).where(keyData)
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
- if (rootHasDraft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
467
+ if (rootHasDraft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
401
468
 
402
- const cqn = INSERT.into(query.INSERT.into).entries(data)
403
- await run(cqn, { event: 'CREATE' })
404
- const result = { ...data, IsActiveEntity: true }
405
- req.data = result //> make keys available via req.data (as with normal crud)
406
- return result
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
- if (req.event === 'DELETE' && draftParams.IsActiveEntity) {
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 !== false) {
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 res = await run(
484
- SELECT.one
485
- .from({ ref: draftRef })
486
- .columns(cols)
487
- .columns([
488
- 'HasActiveEntity',
489
- 'DraftAdministrativeData_DraftUUID',
490
- { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
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: query.SELECT.from.ref }).data(res)
514
- : INSERT.into({ ref: query.SELECT.from.ref }).entries(res),
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.set(
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']) return reject_bypassed_draft(req)
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 hasDraft = !!(await SELECT.one([1]).from({ ref: [draftsRef[0]] }))
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 {} // eslint-disable-line no-empty
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.forEach(key => {
1643
- if (key.name === 'IsActiveEntity' || key.isAssociation || key.virtual) return
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.query.$select || req.req.query.$expand) {
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}`)