@sap/cds 7.3.1 → 7.4.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 +69 -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 +125 -0
- package/apis/{reflect.d.ts → linked.d.ts} +29 -38
- package/apis/models.d.ts +60 -45
- package/apis/ql.d.ts +19 -5
- package/apis/{serve.d.ts → server.d.ts} +59 -33
- package/apis/services.d.ts +76 -147
- package/apis/test.d.ts +1 -1
- package/bin/serve.js +3 -0
- package/lib/compile/cds-compile.js +2 -2
- package/lib/compile/etc/csv.js +2 -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 +7 -4
- 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/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 +27 -9
- 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/serializer/TrustedResourceJsonSerializer.js +8 -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 +28 -9
- package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
- package/libx/_runtime/db/expand/expandCQNToJoin.js +6 -6
- package/libx/_runtime/db/expand/rawToExpanded.js +4 -4
- 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 +171 -84
- 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 +24 -125
- package/libx/odata/afterburner.js +16 -6
- 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
|
}
|
|
@@ -108,8 +112,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
108
112
|
const _etagValidationType = req.headers['if-match']
|
|
109
113
|
? 'if-match'
|
|
110
114
|
: req.headers['if-none-match']
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
? 'if-none-match'
|
|
116
|
+
: undefined
|
|
113
117
|
|
|
114
118
|
const query = _cleansed(req.query, this.model)
|
|
115
119
|
_cleanseParams(req.params, req.target)
|
|
@@ -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
|
|
@@ -177,23 +181,23 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
177
181
|
const read = req.query._target.name.endsWith('.drafts')
|
|
178
182
|
? Read.ownDrafts
|
|
179
183
|
: draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
184
|
+
? Read.all
|
|
185
|
+
: draftParams.IsActiveEntity === true &&
|
|
186
|
+
draftParams.SiblingEntity_IsActiveEntity === null &&
|
|
187
|
+
(draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
|
|
188
|
+
draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
|
|
189
|
+
? Read.lockedByAnotherUser
|
|
190
|
+
: draftParams.IsActiveEntity === true &&
|
|
191
|
+
draftParams.SiblingEntity_IsActiveEntity === null &&
|
|
192
|
+
draftParams.DraftAdministrativeData_InProcessByUser === ''
|
|
193
|
+
? Read.unsavedChangesByAnotherUser
|
|
194
|
+
: draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
|
|
195
|
+
? Read.unchanged
|
|
196
|
+
: draftParams.IsActiveEntity === true
|
|
197
|
+
? Read.onlyActives
|
|
198
|
+
: draftParams.IsActiveEntity === false
|
|
199
|
+
? Read.ownDrafts
|
|
200
|
+
: Read.onlyActives
|
|
197
201
|
const result = await read(run, query)
|
|
198
202
|
return result
|
|
199
203
|
}
|
|
@@ -214,28 +218,27 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
214
218
|
|
|
215
219
|
if (req.event === 'DELETE' && draftParams.IsActiveEntity) {
|
|
216
220
|
const draftsRef = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
|
|
217
|
-
const
|
|
221
|
+
const draftQuery = SELECT.one.from({ ref: draftsRef }).columns([
|
|
218
222
|
{ ref: ['DraftAdministrativeData_DraftUUID'] },
|
|
219
223
|
{
|
|
220
224
|
ref: ['DraftAdministrativeData'],
|
|
221
225
|
expand: [_inProcessByUserXpr(_lock.shiftedNow)]
|
|
222
226
|
}
|
|
223
227
|
])
|
|
228
|
+
|
|
229
|
+
// Deletion of active instance outside draft tree, no need to check for draft
|
|
230
|
+
if (!draftQuery.target?.isDraft) {
|
|
231
|
+
await run(query)
|
|
232
|
+
return req.data
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Deletion of active instance inside draft tree, need to check that no draft exists
|
|
236
|
+
const draft = await draftQuery
|
|
224
237
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
225
238
|
if (inProcessByUser && inProcessByUser !== cds.context.user.id)
|
|
226
239
|
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)
|
|
240
|
+
if (draft) req.reject(403, 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS')
|
|
241
|
+
await run(query)
|
|
239
242
|
return req.data
|
|
240
243
|
}
|
|
241
244
|
|
|
@@ -247,13 +250,13 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
247
250
|
req.reject(400, 'Action "draftActivate" can only be called on the root draft entity')
|
|
248
251
|
}
|
|
249
252
|
const targetDraft = req.target.drafts
|
|
250
|
-
const targetWhere = query.SELECT.from.ref[0].where
|
|
251
253
|
const cols = expandStarStar(targetDraft)
|
|
252
254
|
// Use `run` (since also etags might need to be checked)
|
|
253
255
|
// REVISIT: Find a better approach (`etag` as part of CQN?)
|
|
256
|
+
const draftRef = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
254
257
|
const res = await run(
|
|
255
258
|
SELECT.one
|
|
256
|
-
.from({ ref:
|
|
259
|
+
.from({ ref: draftRef })
|
|
257
260
|
.columns(cols)
|
|
258
261
|
.columns([
|
|
259
262
|
'HasActiveEntity',
|
|
@@ -271,11 +274,13 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
271
274
|
delete res.HasActiveEntity
|
|
272
275
|
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
273
276
|
const result = await run(
|
|
274
|
-
HasActiveEntity
|
|
277
|
+
HasActiveEntity
|
|
278
|
+
? UPDATE({ ref: query.SELECT.from.ref }).data(res)
|
|
279
|
+
: INSERT.into({ ref: query.SELECT.from.ref }).entries(res),
|
|
275
280
|
{ headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
|
|
276
281
|
)
|
|
277
282
|
await _promiseAll([
|
|
278
|
-
DELETE.from(
|
|
283
|
+
DELETE.from({ ref: draftRef }),
|
|
279
284
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
280
285
|
])
|
|
281
286
|
|
|
@@ -407,6 +412,7 @@ const Read = {
|
|
|
407
412
|
})
|
|
408
413
|
return _requested(actives, query)
|
|
409
414
|
},
|
|
415
|
+
|
|
410
416
|
unchanged: async function (run, query) {
|
|
411
417
|
LOG.debug('List Editing Status: Unchanged')
|
|
412
418
|
const draftsQuery = query._drafts
|
|
@@ -421,6 +427,7 @@ const Read = {
|
|
|
421
427
|
})
|
|
422
428
|
return _requested(res, query)
|
|
423
429
|
},
|
|
430
|
+
|
|
424
431
|
ownDrafts: async function (run, query) {
|
|
425
432
|
LOG.debug('List Editing Status: Own Draft')
|
|
426
433
|
|
|
@@ -453,6 +460,7 @@ const Read = {
|
|
|
453
460
|
})
|
|
454
461
|
return _requested(drafts, query)
|
|
455
462
|
},
|
|
463
|
+
|
|
456
464
|
all: async function (run, query) {
|
|
457
465
|
LOG.debug('List Editing Status: All')
|
|
458
466
|
if (!query._drafts) return []
|
|
@@ -518,10 +526,11 @@ const Read = {
|
|
|
518
526
|
}
|
|
519
527
|
_fillIsActiveEntity(row, true, query._target)
|
|
520
528
|
})
|
|
521
|
-
const res = isFirstPage ? [...
|
|
529
|
+
const res = isFirstPage ? [...ownDrafts, ...actives] : actives
|
|
522
530
|
if (query.SELECT.count) res.$count = count
|
|
523
531
|
return _requested(res, query)
|
|
524
532
|
},
|
|
533
|
+
|
|
525
534
|
activesFromDrafts: async function (run, query, { isLocked = true }) {
|
|
526
535
|
const draftsQuery = query._drafts
|
|
527
536
|
const additionalCols = draftsQuery.SELECT.columns
|
|
@@ -550,15 +559,19 @@ const Read = {
|
|
|
550
559
|
})
|
|
551
560
|
return _requested(actives, query)
|
|
552
561
|
},
|
|
562
|
+
|
|
553
563
|
unsavedChangesByAnotherUser: async function (run, query) {
|
|
554
564
|
LOG.debug('List Editing Status: Unsaved Changes by Another User')
|
|
555
565
|
return Read.activesFromDrafts(run, query, { isLocked: false })
|
|
556
566
|
},
|
|
567
|
+
|
|
557
568
|
lockedByAnotherUser: async function (run, query) {
|
|
558
569
|
LOG.debug('List Editing Status: Locked by Another User')
|
|
559
570
|
return Read.activesFromDrafts(run, query, { isLocked: true })
|
|
560
571
|
},
|
|
572
|
+
|
|
561
573
|
whereNotIn: (target, data) => Read.whereIn(target, data, true),
|
|
574
|
+
|
|
562
575
|
whereIn: (target, data, not = false) => {
|
|
563
576
|
const keys = entity_keys(target)
|
|
564
577
|
const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
|
|
@@ -568,6 +581,7 @@ const Read = {
|
|
|
568
581
|
const right = { list: dataArray.map(r => ({ list: keys.map(k => ({ val: r[k] })) })) }
|
|
569
582
|
return [left, ...op, right]
|
|
570
583
|
},
|
|
584
|
+
|
|
571
585
|
complementaryDrafts: (query, _actives) => {
|
|
572
586
|
const actives = Array.isArray(_actives) ? _actives : [_actives]
|
|
573
587
|
if (!actives.length) return []
|
|
@@ -587,7 +601,9 @@ const Read = {
|
|
|
587
601
|
drafts.SELECT.one = undefined
|
|
588
602
|
return drafts
|
|
589
603
|
},
|
|
604
|
+
|
|
590
605
|
_makeArray: data => (Array.isArray(data) ? data : data ? [data] : []),
|
|
606
|
+
|
|
591
607
|
_index: (target, data) => {
|
|
592
608
|
// Indexes the data for fast key access
|
|
593
609
|
const dataArray = Read._makeArray(data)
|
|
@@ -600,6 +616,7 @@ const Read = {
|
|
|
600
616
|
for (const row of dataArray) hashMap.set(hash(row), row)
|
|
601
617
|
return { hashMap, hash }
|
|
602
618
|
},
|
|
619
|
+
|
|
603
620
|
// Calls `cb` for each entry of data with a potential counterpart in otherData
|
|
604
621
|
merge: (target, data, otherData, cb) => {
|
|
605
622
|
const dataArray = Read._makeArray(data)
|
|
@@ -611,6 +628,7 @@ const Read = {
|
|
|
611
628
|
cb(row, other)
|
|
612
629
|
}
|
|
613
630
|
},
|
|
631
|
+
|
|
614
632
|
// Deletes entries of data with a counterpart in otherData
|
|
615
633
|
delete: (target, data, otherData) => {
|
|
616
634
|
if (!Array.isArray(data) || !data.length) return
|
|
@@ -673,6 +691,12 @@ function _cleansed(query, model) {
|
|
|
673
691
|
if (query.SELECT.columns && query._target.drafts)
|
|
674
692
|
draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
|
|
675
693
|
|
|
694
|
+
if (query.SELECT.where && query._target.drafts)
|
|
695
|
+
draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
|
|
696
|
+
|
|
697
|
+
if (query.SELECT.orderBy && query._target.drafts)
|
|
698
|
+
draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
|
|
699
|
+
|
|
676
700
|
if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
|
|
677
701
|
draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
|
|
678
702
|
} else if (draftsQuery._target?.name.endsWith('.drafts')) {
|
|
@@ -712,7 +736,7 @@ function _cleansed(query, model) {
|
|
|
712
736
|
const cleansedRef = ref.map(r => {
|
|
713
737
|
entity = (entity && entity.elements[r.id || r]._target) || model.definitions[r.id || r]
|
|
714
738
|
if (!entity?.drafts) return r
|
|
715
|
-
return r.where ? { ...r, where: _cleanseWhere(r.where, draftParams) } : r
|
|
739
|
+
return r.where ? { ...r, where: _cleanseWhere(r.where, draftParams, DRAFT_ELEMENTS) } : r
|
|
716
740
|
})
|
|
717
741
|
if (q.SELECT) q.SELECT.from = { ...q.SELECT.from, ref: cleansedRef }
|
|
718
742
|
else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
|
|
@@ -730,8 +754,8 @@ function _cleansed(query, model) {
|
|
|
730
754
|
}
|
|
731
755
|
}
|
|
732
756
|
|
|
733
|
-
if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
|
|
734
|
-
if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
|
|
757
|
+
if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams, DRAFT_ELEMENTS)
|
|
758
|
+
if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {}, DRAFT_ELEMENTS)
|
|
735
759
|
if (cqn.columns) cqn.columns = _cleanseCols(cqn.columns, DRAFT_ELEMENTS, target)
|
|
736
760
|
return q
|
|
737
761
|
}
|
|
@@ -800,15 +824,15 @@ function _cleansed(query, model) {
|
|
|
800
824
|
})
|
|
801
825
|
}
|
|
802
826
|
|
|
803
|
-
function _cleanseWhere(xpr, draftParams) {
|
|
827
|
+
function _cleanseWhere(xpr, draftParams, ignoredElements) {
|
|
804
828
|
const cleansed = []
|
|
805
829
|
for (let i = 0; i < xpr.length; ++i) {
|
|
806
830
|
let x = xpr[i]
|
|
807
831
|
const e = x.ref?.[0]
|
|
808
|
-
if (
|
|
832
|
+
if (ignoredElements.has(e) && !xpr[i + 2]) {
|
|
809
833
|
continue
|
|
810
834
|
}
|
|
811
|
-
if (
|
|
835
|
+
if (ignoredElements.has(e) && xpr[i + 2]) {
|
|
812
836
|
let { val } = xpr[i + 2]
|
|
813
837
|
draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
|
|
814
838
|
i += 2
|
|
@@ -817,7 +841,7 @@ function _cleansed(query, model) {
|
|
|
817
841
|
continue
|
|
818
842
|
}
|
|
819
843
|
if (x.xpr) {
|
|
820
|
-
x = { xpr: _cleanseWhere(x.xpr, draftParams) }
|
|
844
|
+
x = { xpr: _cleanseWhere(x.xpr, draftParams, ignoredElements) }
|
|
821
845
|
if (!x.xpr) {
|
|
822
846
|
i += 1
|
|
823
847
|
continue
|
|
@@ -930,18 +954,20 @@ async function onNew(req) {
|
|
|
930
954
|
|
|
931
955
|
async function onEdit(req) {
|
|
932
956
|
LOG.debug('edit active')
|
|
957
|
+
|
|
933
958
|
// use symbol for _draftParams
|
|
934
959
|
const draftParams = req.query[DRAFT_PARAMS]
|
|
935
960
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
|
|
936
961
|
req.reject(400, 'Action "draftEdit" can only be called on the root active entity')
|
|
937
962
|
}
|
|
963
|
+
|
|
938
964
|
if (
|
|
939
965
|
req.target['@Capabilities.UpdateRestrictions.Updatable'] === false ||
|
|
940
966
|
req.target['@insertonly'] ||
|
|
941
967
|
req.target['@readonly']
|
|
942
|
-
)
|
|
968
|
+
) {
|
|
943
969
|
req.reject(405)
|
|
944
|
-
|
|
970
|
+
}
|
|
945
971
|
|
|
946
972
|
if (draftParams.IsActiveEntity !== true) req.reject(400)
|
|
947
973
|
|
|
@@ -963,34 +989,92 @@ async function onEdit(req) {
|
|
|
963
989
|
}
|
|
964
990
|
_addDraftColumns(req.target, cols)
|
|
965
991
|
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
992
|
+
const draftsRef = _redirectRefToDrafts(req.query.SELECT.from.ref, this.model)
|
|
993
|
+
const existingDraft = SELECT.one({ ref: draftsRef }).columns({
|
|
994
|
+
ref: ['DraftAdministrativeData'],
|
|
995
|
+
expand: [_inProcessByUserXpr(_lock.shiftedNow)]
|
|
996
|
+
})
|
|
997
|
+
|
|
969
998
|
// prevent service to check for own user
|
|
970
999
|
existingDraft[DRAFT_PARAMS] = draftParams
|
|
971
1000
|
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1001
|
+
const selectActiveCQN = SELECT.one.from({ ref: req.query.SELECT.from.ref }).columns(cols)
|
|
1002
|
+
selectActiveCQN.SELECT.localized = false
|
|
1003
|
+
|
|
1004
|
+
let res, draft
|
|
1005
|
+
|
|
1006
|
+
// Ensure exclusive access to the record of the active entity by applying a lock,
|
|
1007
|
+
// which effectively prevents the creation or overwriting of duplicate draft entities.
|
|
1008
|
+
// This lock mechanism enforces a strict processing order for active entities,
|
|
1009
|
+
// allowing only one entity to be worked on at any given time.
|
|
1010
|
+
// By using .forUpdate() with a wait value of 0, we immediately lock the record,
|
|
1011
|
+
// ensuring there is no waiting time for other users attempting to edit the same record
|
|
1012
|
+
// concurrently.
|
|
1013
|
+
if (this._datasource === cds.db) {
|
|
1014
|
+
const keys = entity_keys(req.target)
|
|
1015
|
+
const keyData = getKeyData(keys, req.query.SELECT.from.ref[0].where)
|
|
1016
|
+
const rootWhere = keys.reduce((res, key) => {
|
|
1017
|
+
res[key] = keyData[key]
|
|
1018
|
+
return res
|
|
1019
|
+
}, {})
|
|
1020
|
+
const transition = getTransition(req.target, cds.db)
|
|
1021
|
+
|
|
1022
|
+
// gets the underlying target entity, as record locking can't be
|
|
1023
|
+
// applied to localized views
|
|
1024
|
+
const lockTarget = transition.target
|
|
1025
|
+
const lockWhere =
|
|
1026
|
+
transition.mapping.size === 0
|
|
1027
|
+
? rootWhere
|
|
1028
|
+
: (() => {
|
|
1029
|
+
const whereKeys = Object.keys(rootWhere)
|
|
1030
|
+
const w = {}
|
|
1031
|
+
whereKeys.forEach(key => {
|
|
1032
|
+
const mappedKey = transition.mapping.get(key)
|
|
1033
|
+
const lockKey = mappedKey ? mappedKey.ref[0] : key
|
|
1034
|
+
w[lockKey] = rootWhere[key]
|
|
1035
|
+
})
|
|
1036
|
+
return w
|
|
1037
|
+
})()
|
|
1038
|
+
const activeLockCQN = SELECT.from(lockTarget, [1]).where(lockWhere).forUpdate({ wait: 0 })
|
|
1039
|
+
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1040
|
+
|
|
1041
|
+
try {
|
|
1042
|
+
await this.run(activeLockCQN)
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
const draft = await this.run(existingDraft)
|
|
1045
|
+
if (draft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
1046
|
+
req.reject(409, 'ENTITY_LOCKED')
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const cqns = [
|
|
1050
|
+
cds.env.fiori.read_actives_from_db ? this._datasource.run(selectActiveCQN) : this.run(selectActiveCQN),
|
|
1051
|
+
this.run(existingDraft)
|
|
1052
|
+
]
|
|
1053
|
+
|
|
1054
|
+
;[res, draft] = await _promiseAll(cqns)
|
|
1055
|
+
} else {
|
|
1056
|
+
const activeLockCQN = SELECT.from({ ref: req.query.SELECT.from.ref }, [1]).forUpdate({ wait: 0 })
|
|
1057
|
+
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1058
|
+
|
|
1059
|
+
// Locking the underlying database table is effective only when the database is not
|
|
1060
|
+
// hosted on an external service. This is because the active data might be stored in
|
|
1061
|
+
// a separate system.
|
|
1062
|
+
try {
|
|
1063
|
+
await activeLockCQN
|
|
1064
|
+
} catch {} // eslint-disable-line no-empty
|
|
1065
|
+
|
|
1066
|
+
;[res, draft] = await _promiseAll([
|
|
1067
|
+
// REVISIT: inofficial compat flag just in case it breaks something -> do not document
|
|
1068
|
+
cds.env.fiori.read_actives_from_db ? this._datasource.run(selectActiveCQN) : this.run(selectActiveCQN),
|
|
1069
|
+
// no user check must be done here...
|
|
1070
|
+
existingDraft
|
|
1071
|
+
])
|
|
1072
|
+
}
|
|
990
1073
|
|
|
991
1074
|
if (!res) req.reject(404)
|
|
992
1075
|
const preserveChanges = req.context?.data?.PreserveChanges
|
|
993
1076
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
1077
|
+
|
|
994
1078
|
if (draft) {
|
|
995
1079
|
if (inProcessByUser || preserveChanges) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
996
1080
|
const keys = {}
|
|
@@ -1037,14 +1121,17 @@ async function onCancel(req) {
|
|
|
1037
1121
|
.from({ ref: req.query.DELETE.from.ref })
|
|
1038
1122
|
.columns([
|
|
1039
1123
|
'DraftAdministrativeData_DraftUUID',
|
|
1040
|
-
{ ref: ['DraftAdministrativeData'], expand: [
|
|
1124
|
+
{ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] }
|
|
1041
1125
|
])
|
|
1042
1126
|
if (req._etagValidationClause) draftQuery.where(req._etagValidationClause)
|
|
1043
1127
|
// do not add InProcessByUser restriction
|
|
1044
1128
|
const draft = await draftQuery
|
|
1045
1129
|
if (draftParams.IsActiveEntity === false && !draft) req.reject(req._etagValidationType ? 412 : 404)
|
|
1046
|
-
if (draft
|
|
1047
|
-
|
|
1130
|
+
if (draft) {
|
|
1131
|
+
const processByUser = draft.DraftAdministrativeData?.InProcessByUser
|
|
1132
|
+
if (processByUser && processByUser !== cds.context.user.id)
|
|
1133
|
+
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [processByUser])
|
|
1134
|
+
}
|
|
1048
1135
|
const queries = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
|
|
1049
1136
|
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
1050
1137
|
// 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
|