@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.
- package/CHANGELOG.md +79 -21
- package/app/index.js +6 -17
- package/lib/auth/index.js +3 -0
- package/lib/compile/extend.js +9 -4
- package/lib/compile/for/lean_drafts.js +3 -4
- package/lib/compile/load.js +11 -15
- package/lib/compile/minify.js +2 -4
- package/lib/compile/to/sql.js +6 -4
- package/lib/compile/to/yaml.js +1 -1
- package/lib/dbs/cds-deploy.js +7 -13
- package/lib/env/defaults.js +1 -10
- package/lib/env/schemas/cds-package.js +27 -0
- package/lib/env/schemas/cds-rc.js +693 -0
- package/lib/env/schemas/index.js +6 -4
- package/lib/index.js +40 -47
- package/lib/log/cds-error.js +6 -0
- package/lib/log/format/aspects/als.js +1 -0
- package/lib/log/format/json.js +5 -1
- package/lib/ql/Query.js +2 -1
- package/lib/ql/cds-ql.js +1 -2
- package/lib/ql/infer.js +0 -2
- package/lib/req/cds-context.js +1 -1
- package/lib/req/request.js +3 -6
- package/lib/srv/middlewares/trace.js +2 -2
- package/lib/srv/protocols/hcql.js +44 -30
- package/lib/srv/protocols/http.js +60 -0
- package/lib/srv/protocols/index.js +0 -7
- package/lib/srv/protocols/odata-v4.js +8 -2
- package/lib/srv/srv-api.js +129 -62
- package/lib/srv/srv-handlers.js +0 -1
- package/lib/srv/srv-models.js +1 -0
- package/lib/utils/cds-utils.js +26 -0
- package/lib/utils/check-version.js +10 -13
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +22 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +89 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -24
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +1 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ApplyParser.js +3 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +6 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +17 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +22 -2
- package/libx/_runtime/cds-services/services/utils/columns.js +1 -2
- package/libx/_runtime/common/aspects/Association.js +17 -9
- package/libx/_runtime/common/generic/crud.js +13 -22
- package/libx/_runtime/common/generic/etag.js +1 -1
- package/libx/_runtime/common/generic/input.js +9 -1
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/generic/sorting.js +25 -15
- package/libx/_runtime/common/generic/stream.js +2 -16
- package/libx/_runtime/common/i18n/messages.properties +3 -0
- package/libx/_runtime/common/utils/copy.js +5 -0
- package/libx/_runtime/common/utils/cqn.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +4 -3
- package/libx/_runtime/common/utils/csn.js +0 -49
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +5 -5
- package/libx/_runtime/common/utils/generateOnCond.js +50 -25
- package/libx/_runtime/common/utils/resolveView.js +5 -35
- package/libx/_runtime/common/utils/rewriteAsterisks.js +17 -4
- package/libx/_runtime/common/utils/stream.js +16 -15
- package/libx/_runtime/common/utils/streamProp.js +25 -22
- package/libx/_runtime/db/Service.js +27 -8
- package/libx/_runtime/db/generic/input.js +6 -1
- package/libx/_runtime/db/generic/rewrite.js +3 -2
- package/libx/_runtime/db/query/read.js +15 -5
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -11
- package/libx/_runtime/db/utils/columns.js +1 -0
- package/libx/_runtime/db/utils/stream.js +41 -0
- package/libx/_runtime/fiori/generic/read.js +2 -1
- package/libx/_runtime/fiori/generic/readOverDraft.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +209 -55
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/execute.js +53 -14
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +2 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +34 -15
- package/libx/_runtime/messaging/file-based.js +4 -3
- package/libx/_runtime/messaging/redis-messaging.js +2 -1
- package/libx/_runtime/remote/Service.js +2 -1
- package/libx/_runtime/remote/utils/client.js +1 -1
- package/libx/_runtime/sqlite/Service.js +1 -1
- package/libx/_runtime/sqlite/execute.js +17 -5
- package/libx/odata/afterburner.js +58 -19
- package/libx/odata/cqn2odata.js +6 -8
- package/libx/odata/create.js +44 -0
- package/libx/odata/delete.js +25 -0
- package/libx/odata/error.js +8 -3
- package/libx/odata/metadata.js +6 -8
- package/libx/odata/service-document.js +1 -1
- package/libx/odata/update.js +110 -0
- package/libx/odata/utils.js +9 -6
- package/libx/outbox/index.js +74 -89
- package/libx/rest/RestAdapter.js +0 -3
- package/package.json +1 -1
- package/lib/env/schemas/cds-package.json +0 -17
- package/lib/env/schemas/cds-rc.json +0 -740
- 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
|
|
32
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
561
|
-
|
|
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
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
53
|
+
function _getProcedureNameAndSchema(sql) {
|
|
53
54
|
// name delimited with "" allows any character
|
|
54
|
-
const match = sql
|
|
55
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
155
|
+
const { name: procedureName, schema: procedureSchema } = _getProcedureNameAndSchema(sql) || {}
|
|
145
156
|
if (procedureName) {
|
|
146
157
|
try {
|
|
147
|
-
outParameters = await _getProcedureMetadata(procedureName,
|
|
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) || !!
|
|
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] :
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
409
|
+
if (cds.env.features.stream_compat) {
|
|
410
|
+
const val = Object.values(result[0])[0]
|
|
411
|
+
if (val === null) return null
|
|
377
412
|
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
cds.
|
|
30
|
-
|
|
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 (!
|
|
80
|
-
const tenant = req.tenant || req.user
|
|
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
|
-
.
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
224
|
+
const { destination, destinationOptions, jwt, suppressRemoteResponseBody } = options
|
|
225
225
|
try {
|
|
226
226
|
response = await _executeHttpRequest({
|
|
227
227
|
requestConfig,
|