@sap/cds 7.5.2 → 7.6.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 (95) hide show
  1. package/CHANGELOG.md +79 -22
  2. package/app/index.js +1 -1
  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/srvinfo.js +25 -3
  10. package/lib/compile/to/yaml.js +1 -1
  11. package/lib/dbs/cds-deploy.js +7 -13
  12. package/lib/env/defaults.js +1 -10
  13. package/lib/env/schemas/cds-package.js +27 -0
  14. package/lib/env/schemas/cds-rc.js +693 -0
  15. package/lib/env/schemas/index.js +6 -4
  16. package/lib/i18n/localize.js +15 -1
  17. package/lib/index.js +40 -47
  18. package/lib/log/cds-error.js +6 -0
  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/request.js +3 -6
  23. package/lib/srv/middlewares/trace.js +2 -2
  24. package/lib/srv/protocols/hcql.js +44 -30
  25. package/lib/srv/protocols/http.js +60 -0
  26. package/lib/srv/protocols/index.js +0 -7
  27. package/lib/srv/protocols/odata-v4.js +8 -2
  28. package/lib/srv/srv-api.js +129 -62
  29. package/lib/srv/srv-handlers.js +0 -1
  30. package/lib/srv/srv-models.js +1 -0
  31. package/lib/utils/cds-test.js +1 -1
  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/serializer/TrustedResourceJsonSerializer.js +7 -0
  42. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -5
  43. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +17 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +22 -2
  46. package/libx/_runtime/cds-services/services/utils/columns.js +1 -2
  47. package/libx/_runtime/common/aspects/Association.js +17 -9
  48. package/libx/_runtime/common/generic/crud.js +13 -22
  49. package/libx/_runtime/common/generic/etag.js +1 -1
  50. package/libx/_runtime/common/generic/input.js +9 -1
  51. package/libx/_runtime/common/generic/paging.js +3 -3
  52. package/libx/_runtime/common/generic/sorting.js +25 -15
  53. package/libx/_runtime/common/generic/stream.js +2 -16
  54. package/libx/_runtime/common/utils/copy.js +5 -0
  55. package/libx/_runtime/common/utils/cqn.js +1 -1
  56. package/libx/_runtime/common/utils/cqn2cqn4sql.js +4 -3
  57. package/libx/_runtime/common/utils/csn.js +0 -49
  58. package/libx/_runtime/common/utils/foreignKeyPropagations.js +5 -5
  59. package/libx/_runtime/common/utils/generateOnCond.js +50 -25
  60. package/libx/_runtime/common/utils/resolveView.js +5 -44
  61. package/libx/_runtime/common/utils/rewriteAsterisks.js +17 -4
  62. package/libx/_runtime/common/utils/stream.js +16 -15
  63. package/libx/_runtime/common/utils/streamProp.js +25 -22
  64. package/libx/_runtime/db/Service.js +27 -8
  65. package/libx/_runtime/db/generic/input.js +6 -1
  66. package/libx/_runtime/db/generic/rewrite.js +3 -2
  67. package/libx/_runtime/db/query/read.js +15 -5
  68. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -11
  69. package/libx/_runtime/db/utils/columns.js +1 -0
  70. package/libx/_runtime/db/utils/stream.js +41 -0
  71. package/libx/_runtime/fiori/generic/read.js +2 -1
  72. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -1
  73. package/libx/_runtime/fiori/lean-draft.js +216 -59
  74. package/libx/_runtime/hana/Service.js +1 -1
  75. package/libx/_runtime/hana/execute.js +53 -14
  76. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +34 -15
  77. package/libx/_runtime/remote/Service.js +2 -1
  78. package/libx/_runtime/remote/utils/client.js +1 -1
  79. package/libx/_runtime/sqlite/Service.js +1 -1
  80. package/libx/_runtime/sqlite/execute.js +17 -5
  81. package/libx/odata/afterburner.js +58 -19
  82. package/libx/odata/cqn2odata.js +6 -8
  83. package/libx/odata/create.js +44 -0
  84. package/libx/odata/delete.js +25 -0
  85. package/libx/odata/error.js +8 -3
  86. package/libx/odata/metadata.js +6 -8
  87. package/libx/odata/service-document.js +1 -1
  88. package/libx/odata/update.js +110 -0
  89. package/libx/odata/utils.js +9 -6
  90. package/libx/outbox/index.js +48 -78
  91. package/libx/rest/RestAdapter.js +0 -3
  92. package/package.json +1 -1
  93. package/lib/env/schemas/cds-package.json +0 -17
  94. package/lib/env/schemas/cds-rc.json +0 -740
  95. 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,6 +58,9 @@ const DRAFT_ADMIN_ELEMENTS = [
28
58
  'DraftIsProcessedByMe'
29
59
  ]
30
60
 
61
+ const numericCollator = { numeric: true }
62
+ const emptyObject = {}
63
+
31
64
  const _fillIsActiveEntity = (row, IsActiveEntity, target) => {
32
65
  if (target.drafts) row.IsActiveEntity = IsActiveEntity
33
66
  for (const key in target.associations) {
@@ -40,6 +73,18 @@ const _fillIsActiveEntity = (row, IsActiveEntity, target) => {
40
73
  }
41
74
  }
42
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
+
43
88
  /// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
44
89
  const _promiseAll = async array => {
45
90
  const results = await Promise.allSettled(array)
@@ -90,6 +135,36 @@ const _redirectRefToActives = (ref, model) => {
90
135
  return [root.id ? { ...root, id: active.name } : active.name, ...tail]
91
136
  }
92
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
+
93
168
  const h = cds.ApplicationService.prototype.handle
94
169
 
95
170
  /* eslint-disable complexity */
@@ -98,13 +173,14 @@ cds.ApplicationService.prototype.handle = async function (req) {
98
173
 
99
174
  if (
100
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('/') ||
101
178
  req.query[DRAFT_PARAMS] ||
102
179
  (!req.query.SELECT &&
103
180
  !req.query.INSERT &&
104
181
  // !req.query.UPSERT && // skip UPSERTs (might have an additional INSERT)
105
182
  !req.query.UPDATE &&
106
- !req.query.DELETE &&
107
- !req.query.STREAM)
183
+ !req.query.DELETE)
108
184
  ) {
109
185
  return handle(req)
110
186
  }
@@ -203,10 +279,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
203
279
  }
204
280
 
205
281
  if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
206
- if (draftParams.IsActiveEntity && !cds.env.fiori.bypass_draft) req.reject(501)
207
- if (req.data.IsActiveEntity === true && cds.env.fiori.bypass_draft) {
282
+ if (req.event === 'draftPrepare' && draftParams.IsActiveEntity) req.reject(400)
283
+ if (req.event === 'NEW' && req.data?.IsActiveEntity === true) {
284
+ if (!cds.env.fiori.bypass_draft) return reject_bypassed_draft(req)
208
285
  const containsDraftRoot =
209
- this.model.entities[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
286
+ this.model.definitions[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
210
287
  '@Common.DraftRoot.ActivationAction'
211
288
  ]
212
289
 
@@ -380,16 +457,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
380
457
 
381
458
  LOG.debug('patch active')
382
459
 
383
- if (!cds.env.fiori.bypass_draft) {
384
- const msg =
385
- !cds.profiles?.includes('production') &&
386
- '`cds.env.fiori.bypass_draft` must be enabled to support updating active instances.'
387
- return req.reject(403, msg)
388
- }
460
+ if (!cds.env.fiori.bypass_draft) return reject_bypassed_draft(req)
389
461
 
390
462
  const entityRef = query.UPDATE.entity.ref
391
463
 
392
- if (!this.model.entities[entityRef[0].id]['@Common.DraftRoot.ActivationAction']) {
464
+ if (!this.model.definitions[entityRef[0].id]['@Common.DraftRoot.ActivationAction']) {
393
465
  req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
394
466
  }
395
467
 
@@ -401,14 +473,6 @@ cds.ApplicationService.prototype.handle = async function (req) {
401
473
  return req.data
402
474
  }
403
475
 
404
- if (req.event === 'STREAM' && draftParams.IsActiveEntity === false) {
405
- if (query.STREAM.into?.ref) query.STREAM.into.ref = _redirectRefToDrafts(query.STREAM.into.ref, this.model)
406
- else if (query.STREAM.from?.ref) query.STREAM.from.ref = _redirectRefToDrafts(query.STREAM.from.ref, this.model)
407
- const _req = _newReq(req, query, draftParams, { event: req.event })
408
- const result = await handle(_req)
409
- return result
410
- }
411
-
412
476
  req.query = query
413
477
  return handle(req)
414
478
  }
@@ -446,7 +510,12 @@ const Read = {
446
510
  if (_isCount(query)) return run(query)
447
511
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
448
512
  if (!query._target._isDraftEnabled) return run(query)
449
- 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
+ ) {
450
519
  const keys = entity_keys(query._target)
451
520
  for (const key of keys) {
452
521
  if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
@@ -482,7 +551,7 @@ const Read = {
482
551
  const draftsQuery = query._drafts
483
552
  draftsQuery.SELECT.count = undefined
484
553
  draftsQuery.SELECT.orderBy = undefined
485
- draftsQuery.SELECT.limit = false
554
+ draftsQuery.SELECT.limit = null
486
555
  draftsQuery.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
487
556
 
488
557
  const drafts = await draftsQuery.where({ HasActiveEntity: true })
@@ -528,24 +597,71 @@ const Read = {
528
597
  all: async function (run, query) {
529
598
  LOG.debug('List Editing Status: All')
530
599
  if (!query._drafts) return []
600
+
531
601
  query._drafts.SELECT.count = false
532
- 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)
533
603
  const isCount = _isCount(query._drafts)
534
604
  if (isCount) {
535
605
  query._drafts.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
536
606
  }
537
607
  if (!query._drafts.SELECT.columns) query._drafts.SELECT.columns = ['*']
538
- if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity'))
608
+ if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity')) {
539
609
  query._drafts.SELECT.columns.push({ ref: ['HasActiveEntity'] })
610
+ }
540
611
 
541
- 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
+ }
542
627
 
543
- const [ownDrafts, actives] = await Promise.all([
544
- isFirstPage
545
- ? run(query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id))
546
- : [], // we only show drafts on the first page
547
- run(query)
548
- ])
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)
549
665
 
550
666
  const ownNewDrafts = []
551
667
  const ownEditDrafts = []
@@ -554,23 +670,13 @@ const Read = {
554
670
  else ownNewDrafts.push(draft)
555
671
  }
556
672
 
557
- // We can't properly calculate `count`:
558
- // - Not all actives are retrieved (e.g. top = 0), hence there could be more deletes if more actives are requested,
559
- // hence we cannot count deletions based on data.
560
- // - We can't rely on the fact that `HasActiveEntity` always has an active counterpart because the filter
561
- // is applied on draft and active data respectively (you could fetch a draft but not an active instance).
562
- // However, there's not much we can do, so we use use this as a best guess.
563
-
564
- const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
565
- if (isCount) return { $count: count }
673
+ const $count = ownDrafts.length + (isCount ? actives[0]?.$count : actives.$count ?? 0)
674
+ if (isCount) return { $count }
566
675
 
567
676
  Read.merge(query._target, ownDrafts, [], row => {
568
- Object.assign(row, {
569
- HasDraftEntity: false
570
- })
677
+ Object.assign(row, { HasDraftEntity: false })
571
678
  _fillIsActiveEntity(row, false, query._drafts._target)
572
679
  })
573
- Read.delete(query._target, actives, ownEditDrafts)
574
680
  const otherEditDrafts = await Read.complementaryDrafts(query, actives)
575
681
  Read.merge(query._target, actives, otherEditDrafts, (row, other) => {
576
682
  if (other) {
@@ -590,9 +696,64 @@ const Read = {
590
696
  }
591
697
  _fillIsActiveEntity(row, true, query._target)
592
698
  })
593
- const res = isFirstPage ? [...ownDrafts, ...actives] : actives
594
- if (query.SELECT.count) res.$count = count
595
- 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)
596
757
  },
597
758
 
598
759
  activesFromDrafts: async function (run, query, { isLocked = true }) {
@@ -716,7 +877,7 @@ function _cleanseParams(params, target) {
716
877
  if (key === 'IsActiveEntity') {
717
878
  const value = params[key]
718
879
  delete params[key]
719
- if (cds.env.fiori?.draft_compat) Object.defineProperty(params, key, { value, enumerable: false })
880
+ Object.defineProperty(params, key, { value, enumerable: false })
720
881
  }
721
882
  }
722
883
  }
@@ -786,14 +947,8 @@ function _cleansed(query, model) {
786
947
  const target = query._target
787
948
  const q = cds.ql.clone(query)
788
949
 
789
- const ref =
790
- q.SELECT?.from.ref ||
791
- q.UPDATE?.entity.ref ||
792
- q.INSERT?.into.ref ||
793
- q.DELETE?.from.ref ||
794
- q.STREAM?.into?.ref ||
795
- q.STREAM?.from?.ref
796
- 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
797
952
 
798
953
  if (ref) {
799
954
  let entity
@@ -806,8 +961,6 @@ function _cleansed(query, model) {
806
961
  else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
807
962
  else if (q.UPDATE) q.UPDATE.entity = { ...q.UPDATE.entity, ref: cleansedRef }
808
963
  else if (q.INSERT) q.INSERT.into = { ...q.INSERT.into, ref: cleansedRef }
809
- else if (q.STREAM?.from) q.STREAM.from = { ...q.STREAM.from, ref: cleansedRef }
810
- else if (q.STREAM?.into) q.STREAM.into = { ...q.STREAM.into, ref: cleansedRef }
811
964
 
812
965
  // This only works for simple cases of `SiblingEntity`, e.g. `root(ID=1,IsActiveEntity=false)/SiblingEntity`
813
966
  // , check if there are more complicated use cases
@@ -927,7 +1080,7 @@ function expandStarStar(target, recursion = new Map()) {
927
1080
  const columns = []
928
1081
  for (const el in target.elements) {
929
1082
  const element = target.elements[el]
930
- 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] })
931
1084
  if (!element.isComposition || element._target['@odata.draft.enabled'] === false) continue // happens for texts if not @fiori.draft.enabled
932
1085
  const _key = target.name + ':' + el
933
1086
  let cache = recursion.get(_key)
@@ -954,6 +1107,8 @@ async function onNew(req) {
954
1107
  if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
955
1108
  req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
956
1109
 
1110
+ _cleanUpOldDrafts(this, req.tenant)
1111
+
957
1112
  let DraftUUID
958
1113
  if (isDirectAccess) DraftUUID = cds.utils.uuid()
959
1114
  else {
@@ -1035,6 +1190,8 @@ async function onEdit(req) {
1035
1190
 
1036
1191
  if (draftParams.IsActiveEntity !== true) req.reject(400)
1037
1192
 
1193
+ _cleanUpOldDrafts(this, req.tenant)
1194
+
1038
1195
  const DraftUUID = cds.utils.uuid()
1039
1196
 
1040
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
  }
@@ -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`
@@ -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,
@@ -39,7 +39,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
39
39
 
40
40
  // REVISIT: official db api
41
41
  this._insert = this._queries.insert(execute.insert)
42
- this._read = this._queries.read(execute.select, execute.stream)
42
+ this._read = this._queries.read(execute.select, execute.stream, execute.convert)
43
43
  this._update = this._queries.update(execute.update, execute.select)
44
44
  this._delete = this._queries.delete(execute.delete, execute.update)
45
45
  this._run = this._queries.run(this._insert, this._read, this._update, this._delete, execute.cqn, execute.sql)