@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.
- package/CHANGELOG.md +79 -22
- package/app/index.js +1 -1
- 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/srvinfo.js +25 -3
- 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/i18n/localize.js +15 -1
- package/lib/index.js +40 -47
- package/lib/log/cds-error.js +6 -0
- 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/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-test.js +1 -1
- 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/serializer/TrustedResourceJsonSerializer.js +7 -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/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 -44
- 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 +216 -59
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/execute.js +53 -14
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +34 -15
- 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 +48 -78
- 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,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 (
|
|
207
|
-
if (req.
|
|
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.
|
|
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.
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
558
|
-
|
|
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
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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`
|
|
@@ -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,
|
|
@@ -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)
|