@sap/cds 7.5.3 → 7.6.2

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 (101) hide show
  1. package/CHANGELOG.md +79 -21
  2. package/app/index.js +6 -17
  3. package/lib/auth/index.js +3 -0
  4. package/lib/compile/extend.js +9 -4
  5. package/lib/compile/for/lean_drafts.js +3 -4
  6. package/lib/compile/load.js +11 -15
  7. package/lib/compile/minify.js +2 -4
  8. package/lib/compile/to/sql.js +6 -4
  9. package/lib/compile/to/yaml.js +1 -1
  10. package/lib/dbs/cds-deploy.js +7 -13
  11. package/lib/env/defaults.js +1 -10
  12. package/lib/env/schemas/cds-package.js +27 -0
  13. package/lib/env/schemas/cds-rc.js +693 -0
  14. package/lib/env/schemas/index.js +6 -4
  15. package/lib/index.js +40 -47
  16. package/lib/log/cds-error.js +6 -0
  17. package/lib/log/format/aspects/als.js +1 -0
  18. package/lib/log/format/json.js +5 -1
  19. package/lib/ql/Query.js +2 -1
  20. package/lib/ql/cds-ql.js +1 -2
  21. package/lib/ql/infer.js +0 -2
  22. package/lib/req/cds-context.js +1 -1
  23. package/lib/req/request.js +3 -6
  24. package/lib/srv/middlewares/trace.js +2 -2
  25. package/lib/srv/protocols/hcql.js +44 -30
  26. package/lib/srv/protocols/http.js +60 -0
  27. package/lib/srv/protocols/index.js +0 -7
  28. package/lib/srv/protocols/odata-v4.js +8 -2
  29. package/lib/srv/srv-api.js +129 -62
  30. package/lib/srv/srv-handlers.js +0 -1
  31. package/lib/srv/srv-models.js +1 -0
  32. package/lib/utils/cds-utils.js +26 -0
  33. package/lib/utils/check-version.js +10 -13
  34. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +22 -6
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -4
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +89 -21
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +4 -2
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -24
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +1 -7
  40. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ApplyParser.js +3 -3
  41. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
  42. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js +1 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +6 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -5
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -0
  46. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +17 -1
  47. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +22 -2
  48. package/libx/_runtime/cds-services/services/utils/columns.js +1 -2
  49. package/libx/_runtime/common/aspects/Association.js +17 -9
  50. package/libx/_runtime/common/generic/crud.js +13 -22
  51. package/libx/_runtime/common/generic/etag.js +1 -1
  52. package/libx/_runtime/common/generic/input.js +9 -1
  53. package/libx/_runtime/common/generic/paging.js +3 -3
  54. package/libx/_runtime/common/generic/sorting.js +25 -15
  55. package/libx/_runtime/common/generic/stream.js +2 -16
  56. package/libx/_runtime/common/i18n/messages.properties +3 -0
  57. package/libx/_runtime/common/utils/copy.js +5 -0
  58. package/libx/_runtime/common/utils/cqn.js +1 -1
  59. package/libx/_runtime/common/utils/cqn2cqn4sql.js +4 -3
  60. package/libx/_runtime/common/utils/csn.js +0 -49
  61. package/libx/_runtime/common/utils/foreignKeyPropagations.js +5 -5
  62. package/libx/_runtime/common/utils/generateOnCond.js +50 -25
  63. package/libx/_runtime/common/utils/resolveView.js +5 -35
  64. package/libx/_runtime/common/utils/rewriteAsterisks.js +17 -4
  65. package/libx/_runtime/common/utils/stream.js +16 -15
  66. package/libx/_runtime/common/utils/streamProp.js +25 -22
  67. package/libx/_runtime/db/Service.js +27 -8
  68. package/libx/_runtime/db/generic/input.js +6 -1
  69. package/libx/_runtime/db/generic/rewrite.js +3 -2
  70. package/libx/_runtime/db/query/read.js +15 -5
  71. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -11
  72. package/libx/_runtime/db/utils/columns.js +1 -0
  73. package/libx/_runtime/db/utils/stream.js +41 -0
  74. package/libx/_runtime/fiori/generic/read.js +2 -1
  75. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -1
  76. package/libx/_runtime/fiori/lean-draft.js +209 -55
  77. package/libx/_runtime/hana/Service.js +1 -1
  78. package/libx/_runtime/hana/execute.js +53 -14
  79. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +2 -1
  80. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +34 -15
  81. package/libx/_runtime/messaging/file-based.js +4 -3
  82. package/libx/_runtime/messaging/redis-messaging.js +2 -1
  83. package/libx/_runtime/remote/Service.js +2 -1
  84. package/libx/_runtime/remote/utils/client.js +1 -1
  85. package/libx/_runtime/sqlite/Service.js +1 -1
  86. package/libx/_runtime/sqlite/execute.js +17 -5
  87. package/libx/odata/afterburner.js +58 -19
  88. package/libx/odata/cqn2odata.js +6 -8
  89. package/libx/odata/create.js +44 -0
  90. package/libx/odata/delete.js +25 -0
  91. package/libx/odata/error.js +8 -3
  92. package/libx/odata/metadata.js +6 -8
  93. package/libx/odata/service-document.js +1 -1
  94. package/libx/odata/update.js +110 -0
  95. package/libx/odata/utils.js +9 -6
  96. package/libx/outbox/index.js +74 -89
  97. package/libx/rest/RestAdapter.js +0 -3
  98. package/package.json +1 -1
  99. package/lib/env/schemas/cds-package.json +0 -17
  100. package/lib/env/schemas/cds-rc.json +0 -740
  101. package/lib/ql/STREAM.js +0 -90
@@ -2,9 +2,39 @@ const cds = require('../cds'),
2
2
  { Object_keys } = cds.utils
3
3
  const { getTransition } = require('../common/utils/resolveView')
4
4
  const { getKeyData } = require('./utils/where')
5
+ const { getPageSize } = require('../common/generic/paging')
5
6
  const LOG = cds.log('fiori|drafts')
6
7
  const original = Symbol('original')
7
8
  const DRAFT_PARAMS = Symbol('draftParams')
9
+ const AGGREGATION_FUNCTIONS = ['sum', 'min', 'max', 'avg', 'count']
10
+
11
+ const DEL_TIMEOUT = {
12
+ get value() {
13
+ const delTimeout = cds.env.fiori?.draft_deletion_timeout
14
+ const timeout = delTimeout && delTimeout !== false && delTimeout === true ? '30d' : delTimeout
15
+ let parts
16
+ if (typeof timeout === 'string') {
17
+ parts = timeout.match(/^([0-9]+)(d|h)$/)
18
+ if (!parts && !Number(timeout)) throw new Error('Invalid value for `cds.fiori.draft_deletion_timeout`')
19
+ }
20
+ const result =
21
+ parts && parts.length
22
+ ? parts[2] === 'd'
23
+ ? Number(parts[1]) * 1000 * 3600 * 24
24
+ : Number(parts[1]) * 1000 * 3600
25
+ : Number(timeout) || 0
26
+
27
+ Object.defineProperty(DEL_TIMEOUT, 'value', { value: result })
28
+ return result
29
+ }
30
+ }
31
+
32
+ const reject_bypassed_draft = req => {
33
+ const msg =
34
+ !cds.profiles?.includes('production') &&
35
+ '`cds.env.fiori.bypass_draft` must be enabled to support the directly modification of active instances.'
36
+ return req.reject(501, msg)
37
+ }
8
38
 
9
39
  const DRAFT_ELEMENTS = new Set([
10
40
  'IsActiveEntity',
@@ -28,12 +58,8 @@ const DRAFT_ADMIN_ELEMENTS = [
28
58
  'DraftIsProcessedByMe'
29
59
  ]
30
60
 
31
- const reject_bypassed_draft = req => {
32
- const msg =
33
- !cds.profiles?.includes('production') &&
34
- '`cds.env.fiori.bypass_draft` must be enabled to support the directly modification of active instances.'
35
- return req.reject(501, msg)
36
- }
61
+ const numericCollator = { numeric: true }
62
+ const emptyObject = {}
37
63
 
38
64
  const _fillIsActiveEntity = (row, IsActiveEntity, target) => {
39
65
  if (target.drafts) row.IsActiveEntity = IsActiveEntity
@@ -47,6 +73,18 @@ const _fillIsActiveEntity = (row, IsActiveEntity, target) => {
47
73
  }
48
74
  }
49
75
 
76
+ const _filterResultSet = (resultSet, limit, offset) => {
77
+ const pageResultSet = []
78
+
79
+ for (let i = 0; i < resultSet.length; i++) {
80
+ if (i < offset) continue
81
+ pageResultSet.push(resultSet[i])
82
+ if (pageResultSet.length === limit) break
83
+ }
84
+
85
+ return pageResultSet
86
+ }
87
+
50
88
  /// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
51
89
  const _promiseAll = async array => {
52
90
  const results = await Promise.allSettled(array)
@@ -97,6 +135,36 @@ const _redirectRefToActives = (ref, model) => {
97
135
  return [root.id ? { ...root, id: active.name } : active.name, ...tail]
98
136
  }
99
137
 
138
+ let lastCheckMap = new Map()
139
+ const _cleanUpOldDrafts = async (service, tenant) => {
140
+ if (!DEL_TIMEOUT.value) return
141
+
142
+ const invalDate = new Date(Date.now() - DEL_TIMEOUT.value).toISOString()
143
+ const interval = DEL_TIMEOUT.value / 2
144
+ const lastCheck = lastCheckMap.get(tenant)
145
+
146
+ if (lastCheck && Date.now() - lastCheck < Number(interval)) return
147
+
148
+ cds.spawn({ tenant, user: cds.User.privileged }, async () => {
149
+ let invalDrafts = await SELECT.from('DRAFT.DraftAdministrativeData', ['DraftUUID']).where(
150
+ `LastChangeDateTime <`,
151
+ invalDate
152
+ )
153
+ if (!invalDrafts.length) return
154
+ invalDrafts = invalDrafts.map(el => el.DraftUUID)
155
+ const cqns = []
156
+ for (let name in service.model.definitions) {
157
+ const target = service.model.definitions[name]
158
+ if (target.drafts && target['@Common.DraftRoot.ActivationAction'])
159
+ cqns.push(DELETE.from(target.drafts).where(`DraftAdministrativeData_DraftUUID IN`, invalDrafts))
160
+ }
161
+ cqns.push(DELETE.from('DRAFT.DraftAdministrativeData').where(`DraftUUID IN`, invalDrafts))
162
+ await _promiseAll(cqns)
163
+ })
164
+
165
+ lastCheckMap.set(tenant, Date.now())
166
+ }
167
+
100
168
  const h = cds.ApplicationService.prototype.handle
101
169
 
102
170
  /* eslint-disable complexity */
@@ -105,13 +173,14 @@ cds.ApplicationService.prototype.handle = async function (req) {
105
173
 
106
174
  if (
107
175
  !req.query ||
176
+ // REVISIT: Currently all requests in an Object Page to nested composition targets, e.g. Incidents(ID)/conversation are also Draft Requests which seems wrong overkill -> is that required?
177
+ // req.path.includes('/') ||
108
178
  req.query[DRAFT_PARAMS] ||
109
179
  (!req.query.SELECT &&
110
180
  !req.query.INSERT &&
111
181
  // !req.query.UPSERT && // skip UPSERTs (might have an additional INSERT)
112
182
  !req.query.UPDATE &&
113
- !req.query.DELETE &&
114
- !req.query.STREAM)
183
+ !req.query.DELETE)
115
184
  ) {
116
185
  return handle(req)
117
186
  }
@@ -404,14 +473,6 @@ cds.ApplicationService.prototype.handle = async function (req) {
404
473
  return req.data
405
474
  }
406
475
 
407
- if (req.event === 'STREAM' && draftParams.IsActiveEntity === false) {
408
- if (query.STREAM.into?.ref) query.STREAM.into.ref = _redirectRefToDrafts(query.STREAM.into.ref, this.model)
409
- else if (query.STREAM.from?.ref) query.STREAM.from.ref = _redirectRefToDrafts(query.STREAM.from.ref, this.model)
410
- const _req = _newReq(req, query, draftParams, { event: req.event })
411
- const result = await handle(_req)
412
- return result
413
- }
414
-
415
476
  req.query = query
416
477
  return handle(req)
417
478
  }
@@ -449,7 +510,12 @@ const Read = {
449
510
  if (_isCount(query)) return run(query)
450
511
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
451
512
  if (!query._target._isDraftEnabled) return run(query)
452
- if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
513
+ if (
514
+ !query.SELECT.groupBy &&
515
+ query.SELECT.columns &&
516
+ !query.SELECT.columns.some(c => c === '*') &&
517
+ !query.SELECT.columns.some(c => c.func && AGGREGATION_FUNCTIONS.includes(c.func))
518
+ ) {
453
519
  const keys = entity_keys(query._target)
454
520
  for (const key of keys) {
455
521
  if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
@@ -485,7 +551,7 @@ const Read = {
485
551
  const draftsQuery = query._drafts
486
552
  draftsQuery.SELECT.count = undefined
487
553
  draftsQuery.SELECT.orderBy = undefined
488
- draftsQuery.SELECT.limit = false
554
+ draftsQuery.SELECT.limit = null
489
555
  draftsQuery.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
490
556
 
491
557
  const drafts = await draftsQuery.where({ HasActiveEntity: true })
@@ -531,24 +597,71 @@ const Read = {
531
597
  all: async function (run, query) {
532
598
  LOG.debug('List Editing Status: All')
533
599
  if (!query._drafts) return []
600
+
534
601
  query._drafts.SELECT.count = false
535
- query._drafts.SELECT.limit = false // We need all entries for the keys to properly select actives (count)
602
+ query._drafts.SELECT.limit = null // we need all entries for the keys to properly select actives (count)
536
603
  const isCount = _isCount(query._drafts)
537
604
  if (isCount) {
538
605
  query._drafts.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
539
606
  }
540
607
  if (!query._drafts.SELECT.columns) query._drafts.SELECT.columns = ['*']
541
- if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity'))
608
+ if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity')) {
542
609
  query._drafts.SELECT.columns.push({ ref: ['HasActiveEntity'] })
610
+ }
543
611
 
544
- const isFirstPage = !query.SELECT.limit?.offset?.val
612
+ const orderByExpr = query.SELECT.orderBy
613
+ const getOrderByColumns = columns => {
614
+ const selectAll = columns === undefined || columns.includes('*')
615
+ const queryColumns = !selectAll && columns && columns.map(column => column?.ref?.[0]).filter(c => c)
616
+ const newColumns = []
617
+
618
+ for (const column of orderByExpr) {
619
+ if (selectAll || !queryColumns.includes(column.ref.join('_'))) {
620
+ if (column.ref.length === 1 && selectAll) continue
621
+ const columnClone = { ...column }
622
+ delete columnClone.sort
623
+ columnClone.as = columnClone.ref.join('_')
624
+ newColumns.push(columnClone)
625
+ }
626
+ }
545
627
 
546
- const [ownDrafts, actives] = await Promise.all([
547
- isFirstPage
548
- ? run(query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id))
549
- : [], // we only show drafts on the first page
550
- run(query)
551
- ])
628
+ return newColumns
629
+ }
630
+
631
+ let orderByDraftColumns
632
+ if (orderByExpr) {
633
+ orderByDraftColumns = getOrderByColumns(query._drafts.SELECT.columns)
634
+ if (orderByDraftColumns.length) query._drafts.SELECT.columns.push(...orderByDraftColumns)
635
+ }
636
+
637
+ const ownDrafts = await run(
638
+ query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id)
639
+ )
640
+ const draftLength = ownDrafts.length
641
+ const limit = query.SELECT.limit?.rows?.val ?? getPageSize(query._target).max
642
+ const offset = query.SELECT.limit?.offset?.val ?? 0
643
+ query.SELECT.limit = {
644
+ rows: { val: limit + draftLength }, // virtual limit
645
+ offset: { val: Math.max(0, offset - draftLength) } // virtual offset
646
+ }
647
+
648
+ let orderByColumns
649
+ if (orderByExpr) {
650
+ orderByColumns = getOrderByColumns(query.SELECT.columns)
651
+ if (orderByColumns.length) {
652
+ query.SELECT.columns = query.SELECT.columns ?? ['*']
653
+ query.SELECT.columns.push(...orderByColumns)
654
+ }
655
+ }
656
+
657
+ const queryElements = cds.infer.elements4(query.SELECT.columns, query.source)
658
+ const actives = await run(query.where(Read.whereNotIn(query._target, ownDrafts)))
659
+ const removeColumns = (columns, toRemoveCols) => {
660
+ if (!toRemoveCols) return
661
+ for (const c of toRemoveCols) columns.forEach((column, index) => c.as === column.as && columns.splice(index, 1))
662
+ }
663
+ removeColumns(query._drafts.SELECT.columns, orderByDraftColumns)
664
+ removeColumns(query.SELECT.columns, orderByColumns)
552
665
 
553
666
  const ownNewDrafts = []
554
667
  const ownEditDrafts = []
@@ -557,23 +670,13 @@ const Read = {
557
670
  else ownNewDrafts.push(draft)
558
671
  }
559
672
 
560
- // We can't properly calculate `count`:
561
- // - Not all actives are retrieved (e.g. top = 0), hence there could be more deletes if more actives are requested,
562
- // hence we cannot count deletions based on data.
563
- // - We can't rely on the fact that `HasActiveEntity` always has an active counterpart because the filter
564
- // is applied on draft and active data respectively (you could fetch a draft but not an active instance).
565
- // However, there's not much we can do, so we use use this as a best guess.
566
-
567
- const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
568
- if (isCount) return { $count: count }
673
+ const $count = ownDrafts.length + (isCount ? actives[0]?.$count : actives.$count ?? 0)
674
+ if (isCount) return { $count }
569
675
 
570
676
  Read.merge(query._target, ownDrafts, [], row => {
571
- Object.assign(row, {
572
- HasDraftEntity: false
573
- })
677
+ Object.assign(row, { HasDraftEntity: false })
574
678
  _fillIsActiveEntity(row, false, query._drafts._target)
575
679
  })
576
- Read.delete(query._target, actives, ownEditDrafts)
577
680
  const otherEditDrafts = await Read.complementaryDrafts(query, actives)
578
681
  Read.merge(query._target, actives, otherEditDrafts, (row, other) => {
579
682
  if (other) {
@@ -593,9 +696,64 @@ const Read = {
593
696
  }
594
697
  _fillIsActiveEntity(row, true, query._target)
595
698
  })
596
- const res = isFirstPage ? [...ownDrafts, ...actives] : actives
597
- if (query.SELECT.count) res.$count = count
598
- return _requested(res, query)
699
+ const resultSet =
700
+ actives.length > 0 && ownDrafts.length === 0
701
+ ? actives
702
+ : ownDrafts.length > 0 && actives.length === 0
703
+ ? ownDrafts
704
+ : [...ownDrafts, ...actives]
705
+
706
+ // runtime sort required
707
+ if (ownDrafts.length > 0 && actives.length > 0) {
708
+ const locale = cds.context.locale.replaceAll('_', '-')
709
+ const collatorMap = new Map()
710
+ const elementNamesToSort = orderByExpr.map(orderByExp => orderByExp.ref.join('_'))
711
+ for (const elementName of elementNamesToSort) {
712
+ const element = queryElements[elementName]
713
+ let collatorOptions
714
+
715
+ switch (element.type) {
716
+ case 'cds.Integer':
717
+ case 'cds.UInt8':
718
+ case 'cds.Int16':
719
+ case 'cds.Int32':
720
+ case 'cds.Integer64':
721
+ case 'cds.Int64':
722
+ case 'cds.Decimal':
723
+ case 'cds.DecimalFloat':
724
+ case 'cds.Double':
725
+ collatorOptions = numericCollator
726
+ break
727
+
728
+ default:
729
+ collatorOptions = emptyObject
730
+ }
731
+
732
+ const collator = Intl.Collator(locale, collatorOptions)
733
+ collatorMap.set(elementName, collator)
734
+ }
735
+
736
+ const getSortFn =
737
+ (index = 0) =>
738
+ (entityA, entityB) => {
739
+ const orderBy = orderByExpr[index]
740
+ const elementName = elementNamesToSort[index]
741
+ const collator = collatorMap.get(elementName)
742
+ const diff = collator.compare(entityA[elementName], entityB[elementName])
743
+
744
+ if (diff === 0 && index + 1 < orderByExpr.length) return getSortFn(index + 1)(entityA, entityB)
745
+ if (orderBy.sort === 'desc') return diff * -1
746
+ return diff
747
+ }
748
+
749
+ resultSet.sort(getSortFn())
750
+ }
751
+
752
+ let virtualOffset = offset - draftLength
753
+ virtualOffset = virtualOffset > 0 ? draftLength : draftLength + virtualOffset
754
+ const pageResultSet = _filterResultSet(resultSet, limit, virtualOffset)
755
+ if (query.SELECT.count) pageResultSet.$count = ownDrafts.$count ?? 0 + $count
756
+ return _requested(pageResultSet, query)
599
757
  },
600
758
 
601
759
  activesFromDrafts: async function (run, query, { isLocked = true }) {
@@ -789,14 +947,8 @@ function _cleansed(query, model) {
789
947
  const target = query._target
790
948
  const q = cds.ql.clone(query)
791
949
 
792
- const ref =
793
- q.SELECT?.from.ref ||
794
- q.UPDATE?.entity.ref ||
795
- q.INSERT?.into.ref ||
796
- q.DELETE?.from.ref ||
797
- q.STREAM?.into?.ref ||
798
- q.STREAM?.from?.ref
799
- const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE || q.STREAM
950
+ const ref = q.SELECT?.from.ref || q.UPDATE?.entity.ref || q.INSERT?.into.ref || q.DELETE?.from.ref
951
+ const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE
800
952
 
801
953
  if (ref) {
802
954
  let entity
@@ -809,8 +961,6 @@ function _cleansed(query, model) {
809
961
  else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
810
962
  else if (q.UPDATE) q.UPDATE.entity = { ...q.UPDATE.entity, ref: cleansedRef }
811
963
  else if (q.INSERT) q.INSERT.into = { ...q.INSERT.into, ref: cleansedRef }
812
- else if (q.STREAM?.from) q.STREAM.from = { ...q.STREAM.from, ref: cleansedRef }
813
- else if (q.STREAM?.into) q.STREAM.into = { ...q.STREAM.into, ref: cleansedRef }
814
964
 
815
965
  // This only works for simple cases of `SiblingEntity`, e.g. `root(ID=1,IsActiveEntity=false)/SiblingEntity`
816
966
  // , check if there are more complicated use cases
@@ -822,7 +972,7 @@ function _cleansed(query, model) {
822
972
  }
823
973
 
824
974
  if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams, DRAFT_ELEMENTS)
825
- if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {}, DRAFT_ELEMENTS)
975
+ if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseCols(cqn.orderBy, DRAFT_ELEMENTS, target) // allowed to reuse
826
976
  if (cqn.columns) cqn.columns = _cleanseCols(cqn.columns, DRAFT_ELEMENTS, target)
827
977
  return q
828
978
  }
@@ -930,7 +1080,7 @@ function expandStarStar(target, recursion = new Map()) {
930
1080
  const columns = []
931
1081
  for (const el in target.elements) {
932
1082
  const element = target.elements[el]
933
- if (!element.isAssociation && !DRAFT_ELEMENTS.has(el)) columns.push({ ref: [el] })
1083
+ if (!element.isAssociation && !DRAFT_ELEMENTS.has(el) && !element['@odata.draft.skip']) columns.push({ ref: [el] })
934
1084
  if (!element.isComposition || element._target['@odata.draft.enabled'] === false) continue // happens for texts if not @fiori.draft.enabled
935
1085
  const _key = target.name + ':' + el
936
1086
  let cache = recursion.get(_key)
@@ -957,6 +1107,8 @@ async function onNew(req) {
957
1107
  if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
958
1108
  req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
959
1109
 
1110
+ _cleanUpOldDrafts(this, req.tenant)
1111
+
960
1112
  let DraftUUID
961
1113
  if (isDirectAccess) DraftUUID = cds.utils.uuid()
962
1114
  else {
@@ -1038,6 +1190,8 @@ async function onEdit(req) {
1038
1190
 
1039
1191
  if (draftParams.IsActiveEntity !== true) req.reject(400)
1040
1192
 
1193
+ _cleanUpOldDrafts(this, req.tenant)
1194
+
1041
1195
  const DraftUUID = cds.utils.uuid()
1042
1196
 
1043
1197
  // REVISIT: Later optimization if datasource === db: INSERT FROM SELECT
@@ -41,7 +41,7 @@ class HanaDatabase extends DatabaseService {
41
41
 
42
42
  // REVISIT: db api
43
43
  this._insert = this._queries.insert(execute.insert)
44
- this._read = this._queries.read(execute.select, execute.stream)
44
+ this._read = this._queries.read(execute.select, execute.stream, execute.convert)
45
45
  this._update = this._queries.update(execute.update, execute.select)
46
46
  this._delete = this._queries.delete(execute.delete, execute.update)
47
47
  this._run = this._queries.run(this._insert, this._read, this._update, this._delete, execute.cqn, execute.sql)
@@ -14,6 +14,7 @@ const {
14
14
  writeStreamWithHdb,
15
15
  readStreamWithHdb
16
16
  } = require('./streaming')
17
+ const { convertStream } = require('../db/utils/stream')
17
18
 
18
19
  function _cqnToSQL(model, query, user, locale, txTimestamp) {
19
20
  return sqlFactory(
@@ -49,10 +50,19 @@ function _getBinaries(stmt) {
49
50
 
50
51
  const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
51
52
 
52
- function _getProcedureName(sql) {
53
+ function _getProcedureNameAndSchema(sql) {
53
54
  // name delimited with "" allows any character
54
- const match = sql.trim().match(/^call \s*(("\w+"\.)?("(?<delimited>.+)")|(\w+\.)?(?<undelimited>\w+))\s*\(/i)
55
- return match && (match.groups.undelimited ?? match.groups.delimited)
55
+ const match = sql
56
+ .trim()
57
+ .match(
58
+ /^call \s*(("(?<schema_delimited>\w+)"\.)?("(?<delimited>.+)")|(?<schema_undelimited>\w+\.)?(?<undelimited>\w+))\s*\(/i
59
+ )
60
+ return (
61
+ match && {
62
+ name: match.groups.undelimited ?? match.groups.delimited,
63
+ schema: match.groups.schema_delimited || match.groups.schema_undelimited
64
+ }
65
+ )
56
66
  }
57
67
 
58
68
  function _hdbGetResultForProcedure(rows, args, outParameters) {
@@ -98,13 +108,14 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
98
108
  return result
99
109
  }
100
110
 
101
- function _getProcedureMetadata(procedureName, dbc) {
111
+ function _getProcedureMetadata(dbc, name, schema) {
102
112
  return new Promise((resolve, reject) => {
103
113
  // REVISIT: better?
104
114
  if (dbc._closed) return reject(new Error('Transaction is already closed'))
105
-
106
115
  dbc.exec(
107
- `SELECT PARAMETER_NAME FROM SYS.PROCEDURE_PARAMETERS WHERE SCHEMA_NAME = CURRENT_SCHEMA AND PROCEDURE_NAME = '${procedureName}' AND PARAMETER_TYPE IN ('OUT', 'INOUT') ORDER BY POSITION`,
116
+ `SELECT PARAMETER_NAME FROM SYS.PROCEDURE_PARAMETERS WHERE SCHEMA_NAME = ${
117
+ schema?.toUpperCase?.() === 'SYS' ? `'SYS'` : 'CURRENT_SCHEMA'
118
+ } AND PROCEDURE_NAME = '${name}' AND PARAMETER_TYPE IN ('OUT', 'INOUT') ORDER BY POSITION`,
108
119
  (err, res) => {
109
120
  if (err) reject(err)
110
121
  else resolve(res)
@@ -141,10 +152,10 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
141
152
 
142
153
  // procedure call metadata
143
154
  let outParameters
144
- const procedureName = _getProcedureName(sql)
155
+ const { name: procedureName, schema: procedureSchema } = _getProcedureNameAndSchema(sql) || {}
145
156
  if (procedureName) {
146
157
  try {
147
- outParameters = await _getProcedureMetadata(procedureName, dbc)
158
+ outParameters = await _getProcedureMetadata(dbc, procedureName, procedureSchema)
148
159
  } catch (e) {
149
160
  LOG._warn && LOG.warn('Unable to fetch procedure metadata due to error:', e)
150
161
  }
@@ -194,7 +205,7 @@ function _executeSimpleSQL(dbc, sql, values) {
194
205
  }
195
206
 
196
207
  // ensure that stored procedure with parameters is always executed as prepared
197
- if (_hasValues(values) || !!_getProcedureName(sql)) {
208
+ if (_hasValues(values) || !!_getProcedureNameAndSchema(sql)) {
198
209
  _executeAsPreparedStatement(dbc, sql, values, reject, resolve)
199
210
  } else {
200
211
  // REVISIT: better?
@@ -214,7 +225,7 @@ function _executeSimpleSQL(dbc, sql, values) {
214
225
  function _executeSelectSQL(dbc, sql, values, isOne, postMapper) {
215
226
  return _executeSimpleSQL(dbc, sql, values).then(result => {
216
227
  if (isOne) {
217
- result = result.length > 0 ? result[0] : null
228
+ result = result.length > 0 ? result[0] : undefined
218
229
  }
219
230
 
220
231
  return postProcess(result, postMapper)
@@ -360,7 +371,30 @@ function executeGenericCQN(model, dbc, query, user, locale, txTimestamp) {
360
371
  return executePlainSQL(dbc, sql, values)
361
372
  }
362
373
 
363
- async function executeSelectStreamCQN(model, dbc, query, user, locale, txTimestamp) {
374
+ function _convertNames(rs, columns) {
375
+ if (!columns) return rs
376
+
377
+ const result = {}
378
+ for (const key in rs) {
379
+ let key_
380
+ for (let col of columns) {
381
+ const name = col.ref?.[col.ref.length - 1]
382
+ if (name?.toUpperCase() === key) {
383
+ key_ = col.as || name
384
+ break
385
+ }
386
+ }
387
+ if (key_) {
388
+ result[key_] = rs[key]
389
+ } else {
390
+ result[key] = rs[key]
391
+ }
392
+ }
393
+
394
+ return result
395
+ }
396
+
397
+ async function executeSelectStreamCQN({ model, query, dbc, user, locale, txTimestamp }) {
364
398
  const { sql, values = [] } = _cqnToSQL(model, query, user, locale, txTimestamp)
365
399
  let result
366
400
 
@@ -372,10 +406,14 @@ async function executeSelectStreamCQN(model, dbc, query, user, locale, txTimesta
372
406
 
373
407
  if (result.length === 0) return
374
408
 
375
- const val = Object.values(result[0])[0]
376
- if (val === null) return null
409
+ if (cds.env.features.stream_compat) {
410
+ const val = Object.values(result[0])[0]
411
+ if (val === null) return null
377
412
 
378
- return { value: val }
413
+ return { value: val }
414
+ } else {
415
+ return dbc.name === 'hdb' ? result[0] : _convertNames(result[0], query.SELECT?.columns)
416
+ }
379
417
  }
380
418
 
381
419
  module.exports = {
@@ -384,6 +422,7 @@ module.exports = {
384
422
  update: executeUpdateCQN,
385
423
  select: executeSelectCQN,
386
424
  stream: executeSelectStreamCQN,
425
+ convert: convertStream,
387
426
  cqn: executeGenericCQN,
388
427
  sql: executePlainSQL
389
428
  }
@@ -57,7 +57,8 @@ class AMQPWebhookMessaging extends MessagingService {
57
57
  // In case of AMQP and Solace, the `failed` callback must be called
58
58
  // with an error, otherwise there are problems with the redelivery count.
59
59
  failed(new Error('processing failed'))
60
- this.LOG.error('ERROR occured in asynchronous event processing:', e)
60
+ e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
61
+ this.LOG.error(e)
61
62
  }
62
63
  })
63
64
  }
@@ -16,19 +16,30 @@ class EndpointRegistry {
16
16
  this.deployCallbacks = new Map()
17
17
  if (isSecured()) {
18
18
  if (cds.requires.auth.impl) {
19
- const impl = _require(cds.resolve(cds.requires.auth.impl))
20
- paths.forEach(path => cds.app.use(path, impl))
19
+ if (cds.env.requires.middlewares !== false) {
20
+ // we use auth factory here to allow custom auth to be a factory as well
21
+ const custom_auth = require('../../../../lib/auth/index.js')
22
+ paths.forEach(path => cds.app.use(path, custom_auth()))
23
+ } else {
24
+ const impl = _require(cds.resolve(cds.requires.auth.impl))
25
+ paths.forEach(path => cds.app.use(path, impl))
26
+ }
21
27
  } else {
22
- const JWTStrategy = require('../../auth/strategies/JWT.js')
23
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
24
- const passport = _require('passport')
25
- // REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
26
- // In principle, user-facing endpoints might differ from messaging ones.
27
- passport.use(new JWTStrategy(cds.requires.auth.credentials))
28
- paths.forEach(path => {
29
- cds.app.use(path, passport.initialize())
30
- cds.app.use(path, passport.authenticate('JWT', { session: false }))
31
- })
28
+ if (cds.env.requires.middlewares !== false) {
29
+ const jwt_auth = require('../../../../lib/auth/jwt-auth.js')
30
+ paths.forEach(path => cds.app.use(path, jwt_auth(cds.requires.auth)))
31
+ } else {
32
+ const JWTStrategy = require('../../auth/strategies/JWT.js')
33
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
34
+ const passport = _require('passport')
35
+ // REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
36
+ // In principle, user-facing endpoints might differ from messaging ones.
37
+ passport.use(new JWTStrategy(cds.requires.auth.credentials))
38
+ paths.forEach(path => {
39
+ cds.app.use(path, passport.initialize())
40
+ cds.app.use(path, passport.authenticate('JWT', { session: false }))
41
+ })
42
+ }
32
43
  }
33
44
  // unsuccessful auth doesn't automatically reject!
34
45
  paths.forEach(path => {
@@ -72,12 +83,20 @@ class EndpointRegistry {
72
83
  try {
73
84
  if (isSecured() && !req.user.is('emcallback')) return res.sendStatus(403)
74
85
  const queueName = req.query.q
86
+ if (!queueName) {
87
+ LOG.error('Query parameter `q` not found.')
88
+ return res.sendStatus(400)
89
+ }
75
90
  const xAddress = req.headers['x-address']
76
- const topic = xAddress && xAddress.match(/^topic:(.*)/)[1]
91
+ const topic = xAddress && xAddress.match(/^topic:(.*)/)?.[1]
92
+ if (!topic) {
93
+ LOG.error('Incoming message does not contain a topic in header `x-address`: ' + xAddress)
94
+ return res.sendStatus(400)
95
+ }
77
96
  const payload = req.body
78
97
  const cb = this.webhookCallbacks.get(queueName)
79
- if (!topic || !payload || !queueName || !cb) return res.sendStatus(200)
80
- const tenant = req.tenant || req.user.tenant
98
+ if (!cb) return res.sendStatus(200)
99
+ const tenant = req.tenant || req.user?.tenant
81
100
  const other = tenant
82
101
  ? {
83
102
  _: { req, res }, // For `cds.context.http`
@@ -55,9 +55,10 @@ class FileBasedMessaging extends MessagingService {
55
55
  const event = this.subscribedTopics.get(topic)
56
56
  if (!event) return
57
57
  this.tx(tx =>
58
- tx
59
- .emit({ event, ...json, inbound: true })
60
- .catch(e => this.LOG.error('ERROR occured in asynchronous event processing:', e))
58
+ tx.emit({ event, ...json, inbound: true }).catch(e => {
59
+ e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
60
+ this.LOG.error(e)
61
+ })
61
62
  )
62
63
  } else other.push(each + '\n')
63
64
  }
@@ -80,7 +80,8 @@ class RedisMessaging extends cds.MessagingService {
80
80
  try {
81
81
  await this.tx({ user: cds.User.privileged }, tx => tx.emit(msg))
82
82
  } catch (e) {
83
- this.LOG.error('ERROR occured in asynchronous event processing:', e)
83
+ e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
84
+ this.LOG.error(e)
84
85
  }
85
86
  })
86
87
  }
@@ -236,7 +236,8 @@ class RemoteService extends cds.Service {
236
236
 
237
237
  for (const each of this.operations) _addHandlerActionFunction(this, each)
238
238
 
239
- this.on('*', async (req, next) => {
239
+ // IMPORTANT: regular function is used on purpose, don't switch to arrow function.
240
+ this.on('*', async function on_handler(req, next) {
240
241
  const { query } = req
241
242
  if (!query && !(typeof req.path === 'string')) return next()
242
243
 
@@ -221,7 +221,7 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
221
221
  const run = async (requestConfig, options) => {
222
222
  let response
223
223
 
224
- const { destination, destinationOptions, jwt, csrf, csrfInBatch, suppressRemoteResponseBody } = options
224
+ const { destination, destinationOptions, jwt, suppressRemoteResponseBody } = options
225
225
  try {
226
226
  response = await _executeHttpRequest({
227
227
  requestConfig,