@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +69 -3
  2. package/_i18n/i18n_es_MX.properties +110 -0
  3. package/apis/cds.d.ts +13 -12
  4. package/apis/core.d.ts +27 -108
  5. package/apis/cqn.d.ts +15 -18
  6. package/apis/csn.d.ts +95 -60
  7. package/apis/env.d.ts +25 -0
  8. package/apis/events.d.ts +125 -0
  9. package/apis/{reflect.d.ts → linked.d.ts} +29 -38
  10. package/apis/models.d.ts +60 -45
  11. package/apis/ql.d.ts +19 -5
  12. package/apis/{serve.d.ts → server.d.ts} +59 -33
  13. package/apis/services.d.ts +76 -147
  14. package/apis/test.d.ts +1 -1
  15. package/bin/serve.js +3 -0
  16. package/lib/compile/cds-compile.js +2 -2
  17. package/lib/compile/etc/csv.js +2 -1
  18. package/lib/compile/to/edm.js +8 -3
  19. package/lib/compile/to/gql.js +4 -0
  20. package/lib/dbs/cds-deploy.js +52 -4
  21. package/lib/env/cds-requires.js +27 -15
  22. package/lib/env/defaults.js +1 -0
  23. package/lib/env/schemas/index.js +10 -0
  24. package/lib/index.js +7 -4
  25. package/lib/linked/models.js +8 -5
  26. package/lib/ql/CREATE.js +2 -0
  27. package/lib/ql/DELETE.js +1 -0
  28. package/lib/ql/DROP.js +2 -0
  29. package/lib/ql/INSERT.js +2 -22
  30. package/lib/ql/Query.js +59 -22
  31. package/lib/ql/SELECT.js +5 -0
  32. package/lib/ql/STREAM.js +2 -0
  33. package/lib/ql/UPDATE.js +2 -0
  34. package/lib/ql/UPSERT.js +3 -1
  35. package/lib/ql/cds-ql.js +21 -5
  36. package/lib/ql/infer.js +129 -0
  37. package/lib/req/cds-context.js +8 -5
  38. package/lib/srv/cds-connect.js +3 -1
  39. package/lib/utils/axios.js +4 -2
  40. package/lib/utils/data.js +3 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +12 -0
  42. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +27 -9
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +8 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +11 -8
  46. package/libx/_runtime/common/code-ext/worker.js +5 -16
  47. package/libx/_runtime/common/generic/auth/capabilities.js +11 -2
  48. package/libx/_runtime/common/i18n/messages.properties +1 -0
  49. package/libx/_runtime/common/utils/postProcessing.js +1 -1
  50. package/libx/_runtime/common/utils/resolveView.js +28 -9
  51. package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
  52. package/libx/_runtime/db/expand/expandCQNToJoin.js +6 -6
  53. package/libx/_runtime/db/expand/rawToExpanded.js +4 -4
  54. package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -1
  55. package/libx/_runtime/db/sql-builder/UpdateBuilder.js +6 -1
  56. package/libx/_runtime/db/sql-builder/dollar.js +7 -7
  57. package/libx/_runtime/fiori/generic/activate.js +2 -2
  58. package/libx/_runtime/fiori/generic/edit.js +25 -45
  59. package/libx/_runtime/fiori/generic/read.js +3 -5
  60. package/libx/_runtime/fiori/lean-draft.js +171 -84
  61. package/libx/_runtime/fiori/utils/delete.js +7 -1
  62. package/libx/_runtime/fiori/utils/handler.js +4 -6
  63. package/libx/_runtime/fiori/utils/lockInfo.js +27 -0
  64. package/libx/_runtime/fiori/utils/where.js +20 -1
  65. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -2
  66. package/libx/_runtime/messaging/Outbox.js +12 -47
  67. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -3
  68. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +3 -0
  69. package/libx/_runtime/messaging/common-utils/connections.js +1 -1
  70. package/libx/_runtime/messaging/enterprise-messaging.js +12 -13
  71. package/libx/_runtime/messaging/file-based.js +7 -5
  72. package/libx/_runtime/messaging/redis-messaging.js +10 -11
  73. package/libx/_runtime/messaging/service.js +12 -26
  74. package/libx/_runtime/remote/Service.js +52 -36
  75. package/libx/_runtime/remote/utils/client.js +24 -125
  76. package/libx/odata/afterburner.js +16 -6
  77. package/libx/odata/grammar.peggy +26 -7
  78. package/libx/odata/metadata.js +18 -1
  79. package/libx/odata/parser.js +1 -1
  80. package/libx/odata/service-document.js +0 -1
  81. package/libx/odata/utils.js +19 -3
  82. package/libx/{_runtime/messaging/outbox/utils.js → outbox/index.js} +94 -24
  83. package/libx/rest/middleware/parse.js +1 -1
  84. package/package.json +2 -2
  85. package/apis/connect.d.ts +0 -39
  86. package/bin/utils/modules.js +0 -7
  87. package/bin/utils/term.js +0 -56
  88. package/lib/env/schema.js +0 -9
  89. package/lib/linked/queries.js +0 -41
  90. package/lib/srv/protocols/odata-v2-proxy.js +0 -3699
  91. package/libx/common/asserts.js +0 -0
  92. package/libx/common/crud.js +0 -0
  93. package/libx/common/etag.js +0 -0
  94. package/libx/common/localized.js +0 -0
  95. package/libx/common/managed.js +0 -0
  96. package/libx/common/paging.js +0 -0
  97. package/libx/common/readme.md +0 -4
  98. package/libx/common/sorting.js +0 -0
  99. package/libx/common/temporal.js +0 -0
  100. package/libx/connect/auth.js +0 -0
  101. package/libx/connect/perf.js +0 -0
  102. package/libx/connect/readme.md +0 -3
  103. package/libx/fiori/draft/readme.md +0 -1
  104. package/libx/fiori/readme.md +0 -1
  105. package/libx/hana/readme.md +0 -1
  106. package/libx/msg/readme.md +0 -3
  107. package/libx/readme.md +0 -1
  108. package/libx/sqlite/readme.md +0 -1
  109. /package/libx/_runtime/{messaging/common-utils → common/utils}/waitingTime.js +0 -0
  110. /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 e = results.find(r => r.status === 'rejected')
45
- if (e) throw e.reason
46
- return results.map(r => r.value)
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
- const entity_keys = e => {
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.UPSERT || // skip UPSERTs (might have an additional INSERT)
102
- (!req.query.SELECT && !req.query.INSERT && !req.query.UPDATE && !req.query.DELETE && !req.query.STREAM) ||
103
- req.query[DRAFT_PARAMS]
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
- ? 'if-none-match'
112
- : undefined
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
- ? Read.all
181
- : draftParams.IsActiveEntity === true &&
182
- draftParams.SiblingEntity_IsActiveEntity === null &&
183
- (draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
184
- draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
185
- ? Read.lockedByAnotherUser
186
- : draftParams.IsActiveEntity === true &&
187
- draftParams.SiblingEntity_IsActiveEntity === null &&
188
- draftParams.DraftAdministrativeData_InProcessByUser === ''
189
- ? Read.unsavedChangesByAnotherUser
190
- : draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
191
- ? Read.unchanged
192
- : draftParams.IsActiveEntity === true
193
- ? Read.onlyActives
194
- : draftParams.IsActiveEntity === false
195
- ? Read.ownDrafts
196
- : Read.onlyActives
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 draft = await SELECT.one.from({ ref: draftsRef }).columns([
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
- const deletes = [run(DELETE.from({ ref: query.DELETE.from.ref }))]
228
- if (draft)
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: _redirectRefToDrafts(query.SELECT.from.ref, this.model) })
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 ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res),
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(targetDraft).where(targetWhere),
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 ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
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 (DRAFT_ELEMENTS.has(e) && !xpr[i + 2]) {
832
+ if (ignoredElements.has(e) && !xpr[i + 2]) {
809
833
  continue
810
834
  }
811
- if (DRAFT_ELEMENTS.has(e) && xpr[i + 2]) {
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
- const targetWhere = req.query.SELECT.from.ref[0].where
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 existingDraft = SELECT.one(req.target.drafts)
967
- .columns({ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] })
968
- .where(targetWhere)
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 activeCQN = SELECT.one.from(req.target).columns(cols).where(targetWhere)
973
- activeCQN.SELECT.localized = false
974
-
975
- const activeCheck = SELECT.one(req.target).columns([1]).where(targetWhere).forUpdate()
976
- activeCheck[DRAFT_PARAMS] = draftParams
977
- // It's not possible to use `FOR UPDATE` in HANA if the view contains joins/unions. Unfortunately, we can't resolve the table entity
978
- // because we must trigger the app-service request on the target entity (which could be delegated to a remote service).
979
- // The best we can do is to catch a potential error
980
- try {
981
- await activeCheck
982
- } catch {} // eslint-disable-line no-empty
983
-
984
- const [res, draft] = await _promiseAll([
985
- // REVISIT: inofficial compat flag just in case it breaks something -> do not document
986
- cds.env.fiori.read_actives_from_db ? this._datasource.run(activeCQN) : this.run(activeCQN),
987
- // no user check must be done here...
988
- existingDraft
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: [{ ref: ['InProcessByUser'] }] }
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 && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
1047
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draft.DraftAdministrativeData?.InProcessByUser])
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(req.query.DELETE.from.ref[req.query.DELETE.from.ref.length - 1].where)
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 filterKeys = keys => {
245
- return Object.keys(keys).filter(key => {
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
- filterKeys,
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 emit(msg) {
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 super.emit(msg)
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