@sap/cds 7.3.0 → 7.4.0
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 +65 -3
- package/_i18n/i18n_es_MX.properties +110 -0
- package/apis/cds.d.ts +13 -12
- package/apis/core.d.ts +27 -108
- package/apis/cqn.d.ts +15 -18
- package/apis/csn.d.ts +95 -60
- package/apis/env.d.ts +25 -0
- package/apis/events.d.ts +124 -0
- package/apis/{reflect.d.ts → linked.d.ts} +27 -38
- package/apis/models.d.ts +60 -45
- package/apis/ql.d.ts +11 -5
- package/apis/{serve.d.ts → server.d.ts} +57 -31
- package/apis/services.d.ts +74 -145
- package/apis/test.d.ts +1 -1
- package/bin/serve.js +3 -0
- package/lib/compile/cds-compile.js +2 -2
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/to/edm.js +8 -3
- package/lib/compile/to/gql.js +4 -0
- package/lib/dbs/cds-deploy.js +52 -4
- package/lib/env/cds-requires.js +27 -15
- package/lib/env/defaults.js +1 -0
- package/lib/env/schemas/index.js +10 -0
- package/lib/index.js +8 -5
- package/lib/linked/models.js +8 -5
- package/lib/ql/CREATE.js +2 -0
- package/lib/ql/DELETE.js +1 -0
- package/lib/ql/DROP.js +2 -0
- package/lib/ql/INSERT.js +2 -22
- package/lib/ql/Query.js +59 -22
- package/lib/ql/SELECT.js +5 -0
- package/lib/ql/STREAM.js +2 -0
- package/lib/ql/UPDATE.js +2 -0
- package/lib/ql/UPSERT.js +3 -1
- package/lib/ql/cds-ql.js +21 -5
- package/lib/ql/infer.js +129 -0
- package/lib/req/cds-context.js +8 -5
- package/lib/srv/cds-connect.js +3 -1
- package/lib/utils/axios.js +4 -2
- package/lib/utils/cds-utils.js +9 -2
- package/lib/utils/data.js +3 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +12 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +26 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +11 -8
- package/libx/_runtime/common/code-ext/worker.js +5 -16
- package/libx/_runtime/common/generic/auth/capabilities.js +11 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/postProcessing.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +20 -1
- package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
- package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -2
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -1
- package/libx/_runtime/db/sql-builder/UpdateBuilder.js +6 -1
- package/libx/_runtime/db/sql-builder/dollar.js +7 -7
- package/libx/_runtime/fiori/generic/activate.js +2 -2
- package/libx/_runtime/fiori/generic/edit.js +25 -45
- package/libx/_runtime/fiori/generic/read.js +3 -5
- package/libx/_runtime/fiori/lean-draft.js +142 -64
- package/libx/_runtime/fiori/utils/delete.js +7 -1
- package/libx/_runtime/fiori/utils/handler.js +4 -6
- package/libx/_runtime/fiori/utils/lockInfo.js +27 -0
- package/libx/_runtime/fiori/utils/where.js +20 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -2
- package/libx/_runtime/messaging/Outbox.js +12 -47
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -3
- package/libx/_runtime/messaging/common-utils/authorizedRequest.js +3 -0
- package/libx/_runtime/messaging/common-utils/connections.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +12 -13
- package/libx/_runtime/messaging/file-based.js +7 -5
- package/libx/_runtime/messaging/redis-messaging.js +10 -11
- package/libx/_runtime/messaging/service.js +12 -26
- package/libx/_runtime/remote/Service.js +52 -36
- package/libx/_runtime/remote/utils/client.js +22 -123
- package/libx/odata/afterburner.js +14 -5
- package/libx/odata/grammar.peggy +26 -7
- package/libx/odata/metadata.js +18 -1
- package/libx/odata/parser.js +1 -1
- package/libx/odata/service-document.js +0 -1
- package/libx/odata/utils.js +19 -3
- package/libx/{_runtime/messaging/outbox/utils.js → outbox/index.js} +94 -24
- package/libx/rest/middleware/parse.js +1 -1
- package/package.json +2 -2
- package/apis/connect.d.ts +0 -39
- package/bin/utils/modules.js +0 -7
- package/bin/utils/term.js +0 -56
- package/lib/env/schema.js +0 -9
- package/lib/linked/queries.js +0 -41
- package/lib/srv/protocols/odata-v2-proxy.js +0 -3699
- package/libx/common/asserts.js +0 -0
- package/libx/common/crud.js +0 -0
- package/libx/common/etag.js +0 -0
- package/libx/common/localized.js +0 -0
- package/libx/common/managed.js +0 -0
- package/libx/common/paging.js +0 -0
- package/libx/common/readme.md +0 -4
- package/libx/common/sorting.js +0 -0
- package/libx/common/temporal.js +0 -0
- package/libx/connect/auth.js +0 -0
- package/libx/connect/perf.js +0 -0
- package/libx/connect/readme.md +0 -3
- package/libx/fiori/draft/readme.md +0 -1
- package/libx/fiori/readme.md +0 -1
- package/libx/hana/readme.md +0 -1
- package/libx/msg/readme.md +0 -3
- package/libx/readme.md +0 -1
- package/libx/sqlite/readme.md +0 -1
- /package/libx/_runtime/{messaging/common-utils → common/utils}/waitingTime.js +0 -0
- /package/libx/{_runtime/messaging/outbox → outbox}/OutboxRunner.js +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const cds = require('../cds'),
|
|
2
2
|
{ Object_keys } = cds.utils
|
|
3
|
+
const { getTransition } = require('../common/utils/resolveView')
|
|
4
|
+
const { getKeyData } = require('./utils/where')
|
|
3
5
|
const LOG = cds.log('fiori|drafts')
|
|
4
6
|
const original = Symbol('original')
|
|
5
7
|
const DRAFT_PARAMS = Symbol('draftParams')
|
|
@@ -12,9 +14,9 @@ const DRAFT_ELEMENTS = new Set([
|
|
|
12
14
|
'DraftAdministrativeData_DraftUUID',
|
|
13
15
|
'SiblingEntity'
|
|
14
16
|
])
|
|
15
|
-
|
|
17
|
+
const DRAFT_ELEMENTS_WITHOUT_HASACTIVE = new Set(DRAFT_ELEMENTS)
|
|
18
|
+
DRAFT_ELEMENTS_WITHOUT_HASACTIVE.delete('HasActiveEntity')
|
|
16
19
|
const REDUCED_DRAFT_ELEMENTS = new Set(['IsActiveEntity', 'HasDraftEntity', 'SiblingEntity'])
|
|
17
|
-
|
|
18
20
|
const DRAFT_ADMIN_ELEMENTS = [
|
|
19
21
|
'DraftUUID',
|
|
20
22
|
'LastChangedByUser',
|
|
@@ -41,16 +43,14 @@ const _fillIsActiveEntity = (row, IsActiveEntity, target) => {
|
|
|
41
43
|
/// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
|
|
42
44
|
const _promiseAll = async array => {
|
|
43
45
|
const results = await Promise.allSettled(array)
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
return results.map(
|
|
46
|
+
const firstRejected = results.find(response => response.status === 'rejected')
|
|
47
|
+
if (firstRejected) throw firstRejected.reason
|
|
48
|
+
return results.map(result => result.value)
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const _isCount = query => query.SELECT.columns?.length === 1 && query.SELECT.columns[0].func === 'count'
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return Object_keys(e.keys).filter(k => k !== 'IsActiveEntity' && !e.keys[k].isAssociation)
|
|
53
|
-
}
|
|
52
|
+
const entity_keys = entity =>
|
|
53
|
+
Object_keys(entity.keys).filter(key => key !== 'IsActiveEntity' && !entity.keys[key].isAssociation)
|
|
54
54
|
|
|
55
55
|
const _inProcessByUserXpr = lockShiftedNow => ({
|
|
56
56
|
xpr: [
|
|
@@ -98,9 +98,13 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
98
98
|
|
|
99
99
|
if (
|
|
100
100
|
!req.query ||
|
|
101
|
-
req.query
|
|
102
|
-
(!req.query.SELECT &&
|
|
103
|
-
|
|
101
|
+
req.query[DRAFT_PARAMS] ||
|
|
102
|
+
(!req.query.SELECT &&
|
|
103
|
+
!req.query.INSERT &&
|
|
104
|
+
// !req.query.UPSERT && // skip UPSERTs (might have an additional INSERT)
|
|
105
|
+
!req.query.UPDATE &&
|
|
106
|
+
!req.query.DELETE &&
|
|
107
|
+
!req.query.STREAM)
|
|
104
108
|
) {
|
|
105
109
|
return handle(req)
|
|
106
110
|
}
|
|
@@ -119,8 +123,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
119
123
|
const _newReq = (req, query, draftParams, { event, headers }) => {
|
|
120
124
|
// REVISIT: This is a bit hacky -> better way?
|
|
121
125
|
query._target = undefined
|
|
126
|
+
query.target = undefined
|
|
122
127
|
query[DRAFT_PARAMS] = draftParams
|
|
123
|
-
cds.infer(query, this.model.definitions)
|
|
124
128
|
|
|
125
129
|
// REVISIT: This is extremely bad. We should be able to just create a copy without such hacks.
|
|
126
130
|
const _req = cds.Request.for(req._) // REVISIT: this causes req._.data of WRITE reqs copied to READ reqs
|
|
@@ -133,6 +137,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
133
137
|
// If we create a `READ` event based on a modifying request, we delete data
|
|
134
138
|
if (event === 'READ' && req.event !== 'READ') delete _req.data // which we fix here -> but this is an ugly workaround
|
|
135
139
|
|
|
140
|
+
_req.target = cds.infer(query, this.model.definitions)
|
|
136
141
|
_req.query = query
|
|
137
142
|
_req.event =
|
|
138
143
|
event ||
|
|
@@ -141,7 +146,6 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
141
146
|
(query.UPDATE && 'UPDATE') ||
|
|
142
147
|
(query.DELETE && 'DELETE') ||
|
|
143
148
|
req.event
|
|
144
|
-
_req.target = query._target
|
|
145
149
|
_req.params = req.params
|
|
146
150
|
_req._ = Object.assign({}, req._ || {})
|
|
147
151
|
_req._.params = req.params
|
|
@@ -224,18 +228,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
224
228
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
225
229
|
if (inProcessByUser && inProcessByUser !== cds.context.user.id)
|
|
226
230
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [inProcessByUser])
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
deletes.push(
|
|
230
|
-
DELETE.from(req.target.drafts).where({
|
|
231
|
-
DraftAdministrativeData_DraftUUID: draft.DraftAdministrativeData_DraftUUID
|
|
232
|
-
})
|
|
233
|
-
)
|
|
234
|
-
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
235
|
-
deletes.push(
|
|
236
|
-
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
237
|
-
)
|
|
238
|
-
await _promiseAll(deletes)
|
|
231
|
+
if (draft) req.reject(403, 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS')
|
|
232
|
+
await run(DELETE.from({ ref: query.DELETE.from.ref }))
|
|
239
233
|
return req.data
|
|
240
234
|
}
|
|
241
235
|
|
|
@@ -247,13 +241,13 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
247
241
|
req.reject(400, 'Action "draftActivate" can only be called on the root draft entity')
|
|
248
242
|
}
|
|
249
243
|
const targetDraft = req.target.drafts
|
|
250
|
-
const targetWhere = query.SELECT.from.ref[0].where
|
|
251
244
|
const cols = expandStarStar(targetDraft)
|
|
252
245
|
// Use `run` (since also etags might need to be checked)
|
|
253
246
|
// REVISIT: Find a better approach (`etag` as part of CQN?)
|
|
247
|
+
const draftRef = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
254
248
|
const res = await run(
|
|
255
249
|
SELECT.one
|
|
256
|
-
.from({ ref:
|
|
250
|
+
.from({ ref: draftRef })
|
|
257
251
|
.columns(cols)
|
|
258
252
|
.columns([
|
|
259
253
|
'HasActiveEntity',
|
|
@@ -271,11 +265,13 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
271
265
|
delete res.HasActiveEntity
|
|
272
266
|
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
273
267
|
const result = await run(
|
|
274
|
-
HasActiveEntity
|
|
268
|
+
HasActiveEntity
|
|
269
|
+
? UPDATE({ ref: query.SELECT.from.ref }).data(res)
|
|
270
|
+
: INSERT.into({ ref: query.SELECT.from.ref }).entries(res),
|
|
275
271
|
{ headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
|
|
276
272
|
)
|
|
277
273
|
await _promiseAll([
|
|
278
|
-
DELETE.from(
|
|
274
|
+
DELETE.from({ ref: draftRef }),
|
|
279
275
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
280
276
|
])
|
|
281
277
|
|
|
@@ -407,6 +403,7 @@ const Read = {
|
|
|
407
403
|
})
|
|
408
404
|
return _requested(actives, query)
|
|
409
405
|
},
|
|
406
|
+
|
|
410
407
|
unchanged: async function (run, query) {
|
|
411
408
|
LOG.debug('List Editing Status: Unchanged')
|
|
412
409
|
const draftsQuery = query._drafts
|
|
@@ -421,6 +418,7 @@ const Read = {
|
|
|
421
418
|
})
|
|
422
419
|
return _requested(res, query)
|
|
423
420
|
},
|
|
421
|
+
|
|
424
422
|
ownDrafts: async function (run, query) {
|
|
425
423
|
LOG.debug('List Editing Status: Own Draft')
|
|
426
424
|
|
|
@@ -453,6 +451,7 @@ const Read = {
|
|
|
453
451
|
})
|
|
454
452
|
return _requested(drafts, query)
|
|
455
453
|
},
|
|
454
|
+
|
|
456
455
|
all: async function (run, query) {
|
|
457
456
|
LOG.debug('List Editing Status: All')
|
|
458
457
|
if (!query._drafts) return []
|
|
@@ -518,10 +517,11 @@ const Read = {
|
|
|
518
517
|
}
|
|
519
518
|
_fillIsActiveEntity(row, true, query._target)
|
|
520
519
|
})
|
|
521
|
-
const res = isFirstPage ? [...
|
|
520
|
+
const res = isFirstPage ? [...ownDrafts, ...actives] : actives
|
|
522
521
|
if (query.SELECT.count) res.$count = count
|
|
523
522
|
return _requested(res, query)
|
|
524
523
|
},
|
|
524
|
+
|
|
525
525
|
activesFromDrafts: async function (run, query, { isLocked = true }) {
|
|
526
526
|
const draftsQuery = query._drafts
|
|
527
527
|
const additionalCols = draftsQuery.SELECT.columns
|
|
@@ -550,15 +550,19 @@ const Read = {
|
|
|
550
550
|
})
|
|
551
551
|
return _requested(actives, query)
|
|
552
552
|
},
|
|
553
|
+
|
|
553
554
|
unsavedChangesByAnotherUser: async function (run, query) {
|
|
554
555
|
LOG.debug('List Editing Status: Unsaved Changes by Another User')
|
|
555
556
|
return Read.activesFromDrafts(run, query, { isLocked: false })
|
|
556
557
|
},
|
|
558
|
+
|
|
557
559
|
lockedByAnotherUser: async function (run, query) {
|
|
558
560
|
LOG.debug('List Editing Status: Locked by Another User')
|
|
559
561
|
return Read.activesFromDrafts(run, query, { isLocked: true })
|
|
560
562
|
},
|
|
563
|
+
|
|
561
564
|
whereNotIn: (target, data) => Read.whereIn(target, data, true),
|
|
565
|
+
|
|
562
566
|
whereIn: (target, data, not = false) => {
|
|
563
567
|
const keys = entity_keys(target)
|
|
564
568
|
const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
|
|
@@ -568,6 +572,7 @@ const Read = {
|
|
|
568
572
|
const right = { list: dataArray.map(r => ({ list: keys.map(k => ({ val: r[k] })) })) }
|
|
569
573
|
return [left, ...op, right]
|
|
570
574
|
},
|
|
575
|
+
|
|
571
576
|
complementaryDrafts: (query, _actives) => {
|
|
572
577
|
const actives = Array.isArray(_actives) ? _actives : [_actives]
|
|
573
578
|
if (!actives.length) return []
|
|
@@ -587,7 +592,9 @@ const Read = {
|
|
|
587
592
|
drafts.SELECT.one = undefined
|
|
588
593
|
return drafts
|
|
589
594
|
},
|
|
595
|
+
|
|
590
596
|
_makeArray: data => (Array.isArray(data) ? data : data ? [data] : []),
|
|
597
|
+
|
|
591
598
|
_index: (target, data) => {
|
|
592
599
|
// Indexes the data for fast key access
|
|
593
600
|
const dataArray = Read._makeArray(data)
|
|
@@ -600,6 +607,7 @@ const Read = {
|
|
|
600
607
|
for (const row of dataArray) hashMap.set(hash(row), row)
|
|
601
608
|
return { hashMap, hash }
|
|
602
609
|
},
|
|
610
|
+
|
|
603
611
|
// Calls `cb` for each entry of data with a potential counterpart in otherData
|
|
604
612
|
merge: (target, data, otherData, cb) => {
|
|
605
613
|
const dataArray = Read._makeArray(data)
|
|
@@ -611,6 +619,7 @@ const Read = {
|
|
|
611
619
|
cb(row, other)
|
|
612
620
|
}
|
|
613
621
|
},
|
|
622
|
+
|
|
614
623
|
// Deletes entries of data with a counterpart in otherData
|
|
615
624
|
delete: (target, data, otherData) => {
|
|
616
625
|
if (!Array.isArray(data) || !data.length) return
|
|
@@ -673,6 +682,12 @@ function _cleansed(query, model) {
|
|
|
673
682
|
if (query.SELECT.columns && query._target.drafts)
|
|
674
683
|
draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
|
|
675
684
|
|
|
685
|
+
if (query.SELECT.where && query._target.drafts)
|
|
686
|
+
draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
|
|
687
|
+
|
|
688
|
+
if (query.SELECT.orderBy && query._target.drafts)
|
|
689
|
+
draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
|
|
690
|
+
|
|
676
691
|
if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
|
|
677
692
|
draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
|
|
678
693
|
} else if (draftsQuery._target?.name.endsWith('.drafts')) {
|
|
@@ -712,7 +727,7 @@ function _cleansed(query, model) {
|
|
|
712
727
|
const cleansedRef = ref.map(r => {
|
|
713
728
|
entity = (entity && entity.elements[r.id || r]._target) || model.definitions[r.id || r]
|
|
714
729
|
if (!entity?.drafts) return r
|
|
715
|
-
return r.where ? { ...r, where: _cleanseWhere(r.where, draftParams) } : r
|
|
730
|
+
return r.where ? { ...r, where: _cleanseWhere(r.where, draftParams, DRAFT_ELEMENTS) } : r
|
|
716
731
|
})
|
|
717
732
|
if (q.SELECT) q.SELECT.from = { ...q.SELECT.from, ref: cleansedRef }
|
|
718
733
|
else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
|
|
@@ -730,8 +745,8 @@ function _cleansed(query, model) {
|
|
|
730
745
|
}
|
|
731
746
|
}
|
|
732
747
|
|
|
733
|
-
if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
|
|
734
|
-
if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
|
|
748
|
+
if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams, DRAFT_ELEMENTS)
|
|
749
|
+
if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {}, DRAFT_ELEMENTS)
|
|
735
750
|
if (cqn.columns) cqn.columns = _cleanseCols(cqn.columns, DRAFT_ELEMENTS, target)
|
|
736
751
|
return q
|
|
737
752
|
}
|
|
@@ -800,15 +815,15 @@ function _cleansed(query, model) {
|
|
|
800
815
|
})
|
|
801
816
|
}
|
|
802
817
|
|
|
803
|
-
function _cleanseWhere(xpr, draftParams) {
|
|
818
|
+
function _cleanseWhere(xpr, draftParams, ignoredElements) {
|
|
804
819
|
const cleansed = []
|
|
805
820
|
for (let i = 0; i < xpr.length; ++i) {
|
|
806
821
|
let x = xpr[i]
|
|
807
822
|
const e = x.ref?.[0]
|
|
808
|
-
if (
|
|
823
|
+
if (ignoredElements.has(e) && !xpr[i + 2]) {
|
|
809
824
|
continue
|
|
810
825
|
}
|
|
811
|
-
if (
|
|
826
|
+
if (ignoredElements.has(e) && xpr[i + 2]) {
|
|
812
827
|
let { val } = xpr[i + 2]
|
|
813
828
|
draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
|
|
814
829
|
i += 2
|
|
@@ -817,7 +832,7 @@ function _cleansed(query, model) {
|
|
|
817
832
|
continue
|
|
818
833
|
}
|
|
819
834
|
if (x.xpr) {
|
|
820
|
-
x = { xpr: _cleanseWhere(x.xpr, draftParams) }
|
|
835
|
+
x = { xpr: _cleanseWhere(x.xpr, draftParams, ignoredElements) }
|
|
821
836
|
if (!x.xpr) {
|
|
822
837
|
i += 1
|
|
823
838
|
continue
|
|
@@ -930,18 +945,20 @@ async function onNew(req) {
|
|
|
930
945
|
|
|
931
946
|
async function onEdit(req) {
|
|
932
947
|
LOG.debug('edit active')
|
|
948
|
+
|
|
933
949
|
// use symbol for _draftParams
|
|
934
950
|
const draftParams = req.query[DRAFT_PARAMS]
|
|
935
951
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
|
|
936
952
|
req.reject(400, 'Action "draftEdit" can only be called on the root active entity')
|
|
937
953
|
}
|
|
954
|
+
|
|
938
955
|
if (
|
|
939
956
|
req.target['@Capabilities.UpdateRestrictions.Updatable'] === false ||
|
|
940
957
|
req.target['@insertonly'] ||
|
|
941
958
|
req.target['@readonly']
|
|
942
|
-
)
|
|
959
|
+
) {
|
|
943
960
|
req.reject(405)
|
|
944
|
-
|
|
961
|
+
}
|
|
945
962
|
|
|
946
963
|
if (draftParams.IsActiveEntity !== true) req.reject(400)
|
|
947
964
|
|
|
@@ -963,34 +980,92 @@ async function onEdit(req) {
|
|
|
963
980
|
}
|
|
964
981
|
_addDraftColumns(req.target, cols)
|
|
965
982
|
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
983
|
+
const draftsRef = _redirectRefToDrafts(req.query.SELECT.from.ref, this.model)
|
|
984
|
+
const existingDraft = SELECT.one({ ref: draftsRef }).columns({
|
|
985
|
+
ref: ['DraftAdministrativeData'],
|
|
986
|
+
expand: [_inProcessByUserXpr(_lock.shiftedNow)]
|
|
987
|
+
})
|
|
988
|
+
|
|
969
989
|
// prevent service to check for own user
|
|
970
990
|
existingDraft[DRAFT_PARAMS] = draftParams
|
|
971
991
|
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
992
|
+
const selectActiveCQN = SELECT.one.from({ ref: req.query.SELECT.from.ref }).columns(cols)
|
|
993
|
+
selectActiveCQN.SELECT.localized = false
|
|
994
|
+
|
|
995
|
+
let res, draft
|
|
996
|
+
|
|
997
|
+
// Ensure exclusive access to the record of the active entity by applying a lock,
|
|
998
|
+
// which effectively prevents the creation or overwriting of duplicate draft entities.
|
|
999
|
+
// This lock mechanism enforces a strict processing order for active entities,
|
|
1000
|
+
// allowing only one entity to be worked on at any given time.
|
|
1001
|
+
// By using .forUpdate() with a wait value of 0, we immediately lock the record,
|
|
1002
|
+
// ensuring there is no waiting time for other users attempting to edit the same record
|
|
1003
|
+
// concurrently.
|
|
1004
|
+
if (this._datasource === cds.db) {
|
|
1005
|
+
const keys = entity_keys(req.target)
|
|
1006
|
+
const keyData = getKeyData(keys, req.query.SELECT.from.ref[0].where)
|
|
1007
|
+
const rootWhere = keys.reduce((res, key) => {
|
|
1008
|
+
res[key] = keyData[key]
|
|
1009
|
+
return res
|
|
1010
|
+
}, {})
|
|
1011
|
+
const transition = getTransition(req.target, cds.db)
|
|
1012
|
+
|
|
1013
|
+
// gets the underlying target entity, as record locking can't be
|
|
1014
|
+
// applied to localized views
|
|
1015
|
+
const lockTarget = transition.target
|
|
1016
|
+
const lockWhere =
|
|
1017
|
+
transition.mapping.size === 0
|
|
1018
|
+
? rootWhere
|
|
1019
|
+
: (() => {
|
|
1020
|
+
const whereKeys = Object.keys(rootWhere)
|
|
1021
|
+
const w = {}
|
|
1022
|
+
whereKeys.forEach(key => {
|
|
1023
|
+
const mappedKey = transition.mapping.get(key)
|
|
1024
|
+
const lockKey = mappedKey ? mappedKey.ref[0] : key
|
|
1025
|
+
w[lockKey] = rootWhere[key]
|
|
1026
|
+
})
|
|
1027
|
+
return w
|
|
1028
|
+
})()
|
|
1029
|
+
const activeLockCQN = SELECT.from(lockTarget, [1]).where(lockWhere).forUpdate({ wait: 0 })
|
|
1030
|
+
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1031
|
+
|
|
1032
|
+
try {
|
|
1033
|
+
await this.run(activeLockCQN)
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
const draft = await this.run(existingDraft)
|
|
1036
|
+
if (draft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
1037
|
+
req.reject(409, 'ENTITY_LOCKED')
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const cqns = [
|
|
1041
|
+
cds.env.fiori.read_actives_from_db ? this._datasource.run(selectActiveCQN) : this.run(selectActiveCQN),
|
|
1042
|
+
this.run(existingDraft)
|
|
1043
|
+
]
|
|
1044
|
+
|
|
1045
|
+
;[res, draft] = await _promiseAll(cqns)
|
|
1046
|
+
} else {
|
|
1047
|
+
const activeLockCQN = SELECT.from({ ref: req.query.SELECT.from.ref }, [1]).forUpdate({ wait: 0 })
|
|
1048
|
+
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1049
|
+
|
|
1050
|
+
// Locking the underlying database table is effective only when the database is not
|
|
1051
|
+
// hosted on an external service. This is because the active data might be stored in
|
|
1052
|
+
// a separate system.
|
|
1053
|
+
try {
|
|
1054
|
+
await activeLockCQN
|
|
1055
|
+
} catch {} // eslint-disable-line no-empty
|
|
1056
|
+
|
|
1057
|
+
;[res, draft] = await _promiseAll([
|
|
1058
|
+
// REVISIT: inofficial compat flag just in case it breaks something -> do not document
|
|
1059
|
+
cds.env.fiori.read_actives_from_db ? this._datasource.run(selectActiveCQN) : this.run(selectActiveCQN),
|
|
1060
|
+
// no user check must be done here...
|
|
1061
|
+
existingDraft
|
|
1062
|
+
])
|
|
1063
|
+
}
|
|
990
1064
|
|
|
991
1065
|
if (!res) req.reject(404)
|
|
992
1066
|
const preserveChanges = req.context?.data?.PreserveChanges
|
|
993
1067
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
1068
|
+
|
|
994
1069
|
if (draft) {
|
|
995
1070
|
if (inProcessByUser || preserveChanges) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
996
1071
|
const keys = {}
|
|
@@ -1037,14 +1112,17 @@ async function onCancel(req) {
|
|
|
1037
1112
|
.from({ ref: req.query.DELETE.from.ref })
|
|
1038
1113
|
.columns([
|
|
1039
1114
|
'DraftAdministrativeData_DraftUUID',
|
|
1040
|
-
{ ref: ['DraftAdministrativeData'], expand: [
|
|
1115
|
+
{ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] }
|
|
1041
1116
|
])
|
|
1042
1117
|
if (req._etagValidationClause) draftQuery.where(req._etagValidationClause)
|
|
1043
1118
|
// do not add InProcessByUser restriction
|
|
1044
1119
|
const draft = await draftQuery
|
|
1045
1120
|
if (draftParams.IsActiveEntity === false && !draft) req.reject(req._etagValidationType ? 412 : 404)
|
|
1046
|
-
if (draft
|
|
1047
|
-
|
|
1121
|
+
if (draft) {
|
|
1122
|
+
const processByUser = draft.DraftAdministrativeData?.InProcessByUser
|
|
1123
|
+
if (processByUser && processByUser !== cds.context.user.id)
|
|
1124
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [processByUser])
|
|
1125
|
+
}
|
|
1048
1126
|
const queries = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
|
|
1049
1127
|
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
1050
1128
|
// only for draft root
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
+
const getError = require('../../common/error')
|
|
2
3
|
const { SELECT, DELETE } = cds.ql
|
|
3
4
|
|
|
4
5
|
const { isDraftRootEntity } = require('./csn')
|
|
@@ -39,8 +40,13 @@ const deleteDraft = async (req, srv, includingActive = false) => {
|
|
|
39
40
|
const dbtx = cds.tx(req)
|
|
40
41
|
const definitions = srv.model.definitions
|
|
41
42
|
|
|
43
|
+
const where = req.query.DELETE.from.ref?.[req.query.DELETE.from.ref.length - 1].where
|
|
44
|
+
if (!where) {
|
|
45
|
+
req.reject(getError(500, 'Invalid delete draft request'))
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
// REVISIT: how to handle delete of to 1 assoc
|
|
43
|
-
const keys = extractKeyConditions(
|
|
49
|
+
const keys = extractKeyConditions(where)
|
|
44
50
|
|
|
45
51
|
// IsActiveEntity is deleted from where clause in auth.js, hence keys.IsActiveEntity is undefined here.
|
|
46
52
|
// Intentional?
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
+
const { Object_keys } = cds.utils
|
|
2
3
|
const { UPDATE, SELECT } = cds.ql
|
|
3
4
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
4
5
|
const { ensureNoDraftsSuffix, ensureDraftsSuffix, ensureUnlocalized } = require('../../common/utils/draft')
|
|
@@ -241,11 +242,8 @@ const draftIsLocked = lastChangedAt => {
|
|
|
241
242
|
return DRAFT_CANCEL_TIMEOUT_IN_MS > Date.now() - Date.parse(lastChangedAt)
|
|
242
243
|
}
|
|
243
244
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
return key !== 'IsActiveEntity' && !keys[key]._isAssociationStrict
|
|
247
|
-
})
|
|
248
|
-
}
|
|
245
|
+
const entity_keys = entity =>
|
|
246
|
+
Object_keys(entity.keys).filter(key => key !== 'IsActiveEntity' && !entity.keys[key]._isAssociationStrict)
|
|
249
247
|
|
|
250
248
|
module.exports = {
|
|
251
249
|
getSubCQNs,
|
|
@@ -260,7 +258,7 @@ module.exports = {
|
|
|
260
258
|
proxifyToNoDraftsName,
|
|
261
259
|
addColumnAlias,
|
|
262
260
|
replaceRefWithDraft,
|
|
263
|
-
|
|
261
|
+
entity_keys,
|
|
264
262
|
getDeleteDraftAdminCqn,
|
|
265
263
|
getCompositionTargets
|
|
266
264
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const { getTransition } = require('../../common/utils/resolveView')
|
|
3
|
+
const { getKeyData, getLockWhere } = require('../utils/where')
|
|
4
|
+
const { entity_keys } = require('../utils/handler')
|
|
5
|
+
|
|
6
|
+
const getLockInfo = (req, dataSource) => {
|
|
7
|
+
const keys = entity_keys(req.target)
|
|
8
|
+
const keyData = getKeyData(keys, req.query.SELECT.from.ref[0].where)
|
|
9
|
+
const rootWhere = keys.reduce((res, key) => {
|
|
10
|
+
res[key] = keyData[key]
|
|
11
|
+
return res
|
|
12
|
+
}, {})
|
|
13
|
+
|
|
14
|
+
if (dataSource === undefined || dataSource === cds.db) {
|
|
15
|
+
const transition = getTransition(req.target, cds.db)
|
|
16
|
+
|
|
17
|
+
// gets the underlying target entity, as record locking can't be
|
|
18
|
+
// applied to localized views
|
|
19
|
+
const target = transition.target
|
|
20
|
+
const where = getLockWhere(rootWhere, transition.mapping)
|
|
21
|
+
return { target, where, rootWhere }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { target: req.target, where: req.query.SELECT.from.ref[0].where, rootWhere }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = getLockInfo
|
|
@@ -26,19 +26,23 @@ const _calculateSpliceArgs = (index, whereCondition, isXpr = false) => {
|
|
|
26
26
|
if (whereCondition[index - 1] in AND_OR) {
|
|
27
27
|
return { index: index - 1, count: 1 + len }
|
|
28
28
|
}
|
|
29
|
+
|
|
29
30
|
if (whereCondition[index + len] in AND_OR) {
|
|
30
31
|
return { index: index, count: len + 1 }
|
|
31
32
|
}
|
|
33
|
+
|
|
32
34
|
if (whereCondition[index - 1] === '(' && whereCondition[index + len] === ')') {
|
|
33
35
|
if (whereCondition[index - 2] in AND_OR) {
|
|
34
36
|
return { index: index - 2, count: len + 3 }
|
|
35
37
|
}
|
|
38
|
+
|
|
36
39
|
if (whereCondition[index + len + 1] in AND_OR) {
|
|
37
40
|
return { index: index - 1, count: len + 3 }
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
return { index: index - 1, count: len + 2 }
|
|
41
44
|
}
|
|
45
|
+
|
|
42
46
|
return { index: index, count: len }
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -224,6 +228,20 @@ const getKeysCondition = req => {
|
|
|
224
228
|
return where
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
const getLockWhere = (where, columnsMap) => {
|
|
232
|
+
if (columnsMap.size === 0) return where
|
|
233
|
+
const whereKeys = Object.keys(where)
|
|
234
|
+
const lockWhere = {}
|
|
235
|
+
|
|
236
|
+
whereKeys.forEach(key => {
|
|
237
|
+
const mappedKey = columnsMap.get(key)
|
|
238
|
+
const lockKey = mappedKey ? mappedKey.ref[0] : key
|
|
239
|
+
lockWhere[lockKey] = where[key]
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
return lockWhere
|
|
243
|
+
}
|
|
244
|
+
|
|
227
245
|
module.exports = {
|
|
228
246
|
deleteCondition,
|
|
229
247
|
readAndDeleteKeywords,
|
|
@@ -231,5 +249,6 @@ module.exports = {
|
|
|
231
249
|
isActiveEntityRequested,
|
|
232
250
|
getKeyData,
|
|
233
251
|
extractKeyConditions,
|
|
234
|
-
getKeysCondition
|
|
252
|
+
getKeysCondition,
|
|
253
|
+
getLockWhere
|
|
235
254
|
}
|
|
@@ -24,7 +24,8 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
24
24
|
return super.init()
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async
|
|
27
|
+
async handle(msg) {
|
|
28
|
+
if (msg.inbound) return super.handle(msg)
|
|
28
29
|
const _msg = this.message4(msg)
|
|
29
30
|
const client = this.getClient()
|
|
30
31
|
await this.queued(() => {})()
|
|
@@ -50,7 +51,7 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
50
51
|
if (!msg._) msg._ = {}
|
|
51
52
|
msg._.topic = _topic
|
|
52
53
|
try {
|
|
53
|
-
await
|
|
54
|
+
await this.tx({ user: cds.User.privileged, tenant: msg.tenant, _: msg._ }, tx => tx.emit(msg))
|
|
54
55
|
done()
|
|
55
56
|
} catch (e) {
|
|
56
57
|
// In case of AMQP and Solace, the `failed` callback must be called
|
|
@@ -1,53 +1,18 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
|
+
const LOG = cds.log()
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
processMessages,
|
|
5
|
-
registerMessageProcessor,
|
|
6
|
-
writeInOutbox,
|
|
7
|
-
hasPersistentOutbox,
|
|
8
|
-
isUnrecoverable
|
|
9
|
-
} = require('./outbox/utils')
|
|
4
|
+
let logged
|
|
10
5
|
|
|
11
|
-
class
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// REVISIT: Also allow to overwrite this.send
|
|
18
|
-
this._emitImmediate = this.emit
|
|
19
|
-
this.emit = async function (...args) {
|
|
20
|
-
const msg = typeof args[0] === 'object' ? args[0] : { event: args[0], data: args[1], headers: args[2] }
|
|
21
|
-
const context = this.context || cds.context
|
|
22
|
-
if (this.options.outbox && context) {
|
|
23
|
-
const outboxOpts = Object.assign(
|
|
24
|
-
{},
|
|
25
|
-
(typeof cds.requires.outbox === 'object' && cds.requires.outbox) || {},
|
|
26
|
-
(this.options && typeof this.options.outbox === 'object' && this.options.outbox) || {}
|
|
27
|
-
)
|
|
28
|
-
if (hasPersistentOutbox(this, context.tenant)) {
|
|
29
|
-
// returns true if not yet registered
|
|
30
|
-
if (registerMessageProcessor(this.name, context)) {
|
|
31
|
-
context.on('succeeded', () => processMessages(this, context.tenant, outboxOpts))
|
|
32
|
-
}
|
|
33
|
-
await writeInOutbox(this.name, msg, context)
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
// Revisit: Also allow maxAttempts?
|
|
37
|
-
context.on('succeeded', async () => {
|
|
38
|
-
try {
|
|
39
|
-
await this._emitImmediate(msg)
|
|
40
|
-
} catch (e) {
|
|
41
|
-
LOG.error('Emit failed', { event: msg.event, cause: e })
|
|
42
|
-
// opts.crashOnError is not official!!!
|
|
43
|
-
if (isUnrecoverable(this, e) && outboxOpts.crashOnError !== false) cds.exit(1)
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
return this._emitImmediate(msg)
|
|
6
|
+
module.exports = class Outbox extends cds.Service {
|
|
7
|
+
constructor(...args) {
|
|
8
|
+
if (!logged && LOG._warn) {
|
|
9
|
+
// prettier-ignore
|
|
10
|
+
LOG.warn('Internal class `OutboxService` is deprecated and will be removed. Services are outboxable via config or `cds.outboxed()`.')
|
|
11
|
+
logged = true
|
|
49
12
|
}
|
|
13
|
+
|
|
14
|
+
super(...args)
|
|
15
|
+
|
|
16
|
+
if (this.options.outbox) return cds.outboxed(this)
|
|
50
17
|
}
|
|
51
18
|
}
|
|
52
|
-
|
|
53
|
-
module.exports = OutboxService
|
|
@@ -2,7 +2,6 @@ const cds = require('../../cds.js')
|
|
|
2
2
|
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
3
3
|
const ClientAmqp = require('@sap/xb-msg-amqp-v100').Client
|
|
4
4
|
const { connect, disconnect } = require('./connections')
|
|
5
|
-
const { hasPersistentOutbox } = require('../outbox/utils')
|
|
6
5
|
|
|
7
6
|
const addDataListener = (client, queue, prefix, cb) =>
|
|
8
7
|
new Promise(resolve => {
|
|
@@ -79,8 +78,7 @@ class AMQPClient {
|
|
|
79
78
|
async emit(msg) {
|
|
80
79
|
if (!this.client) await this.connect()
|
|
81
80
|
// REVISIT: Is this a robust way to find out if the connection is working?
|
|
82
|
-
if (
|
|
83
|
-
throw new Error('AMQP: Sender is not open')
|
|
81
|
+
if (msg._fromOutbox && !this.sender.opened()) throw new Error('AMQP: Sender is not open')
|
|
84
82
|
await emit(msg, this.stream, this.prefix.topic, this.service.LOG)
|
|
85
83
|
if (!this.keepAlive) return this.disconnect()
|
|
86
84
|
}
|