@sap/cds 8.8.3 → 8.9.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 (73) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/_i18n/i18n_en_US_saptrc.properties +3 -0
  3. package/bin/colors.js +2 -0
  4. package/bin/test.js +103 -75
  5. package/eslint.config.mjs +16 -4
  6. package/lib/compile/for/lean_drafts.js +4 -0
  7. package/lib/compile/parse.js +26 -6
  8. package/lib/env/cds-env.js +3 -1
  9. package/lib/env/cds-requires.js +0 -3
  10. package/lib/env/schemas/cds-rc.js +11 -0
  11. package/lib/log/format/aspects/cls.js +2 -1
  12. package/lib/log/format/json.js +1 -1
  13. package/lib/plugins.js +2 -3
  14. package/lib/ql/SELECT.js +2 -1
  15. package/lib/ql/cds-ql.js +2 -0
  16. package/lib/ql/cds.ql-predicates.js +6 -4
  17. package/lib/ql/resolve.js +46 -0
  18. package/lib/req/validate.js +1 -0
  19. package/lib/srv/bindings.js +64 -43
  20. package/lib/srv/cds-connect.js +1 -1
  21. package/lib/srv/cds-serve.js +2 -2
  22. package/lib/srv/middlewares/auth/ias-auth.js +2 -0
  23. package/lib/srv/protocols/http.js +2 -2
  24. package/lib/srv/protocols/index.js +1 -1
  25. package/lib/srv/protocols/odata-v4.js +0 -1
  26. package/lib/srv/srv-tx.js +1 -1
  27. package/lib/test/cds-test.js +3 -4
  28. package/lib/utils/cds-utils.js +19 -19
  29. package/lib/utils/colors.js +46 -45
  30. package/lib/utils/csv-reader.js +5 -5
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -2
  32. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  33. package/libx/_runtime/common/Service.js +4 -2
  34. package/libx/_runtime/common/composition/data.js +1 -2
  35. package/libx/_runtime/common/composition/tree.js +6 -4
  36. package/libx/_runtime/common/generic/sorting.js +6 -2
  37. package/libx/_runtime/common/utils/cqn2cqn4sql.js +6 -7
  38. package/libx/_runtime/common/utils/differ.js +1 -1
  39. package/libx/_runtime/common/utils/draft.js +1 -1
  40. package/libx/_runtime/common/utils/foreignKeyPropagations.js +6 -2
  41. package/libx/_runtime/common/utils/keys.js +13 -84
  42. package/libx/_runtime/common/utils/propagateForeignKeys.js +4 -3
  43. package/libx/_runtime/common/utils/resolveView.js +96 -102
  44. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  45. package/libx/_runtime/common/utils/stream.js +2 -3
  46. package/libx/_runtime/db/utils/columns.js +1 -1
  47. package/libx/_runtime/fiori/lean-draft.js +11 -7
  48. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  49. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -2
  50. package/libx/_runtime/messaging/file-based.js +6 -6
  51. package/libx/_runtime/messaging/kafka.js +5 -7
  52. package/libx/_runtime/messaging/redis-messaging.js +1 -1
  53. package/libx/_runtime/messaging/service.js +11 -4
  54. package/libx/_runtime/remote/Service.js +13 -5
  55. package/libx/_runtime/remote/utils/client.js +1 -0
  56. package/libx/_runtime/ucl/Service.js +135 -126
  57. package/libx/common/utils/path.js +34 -22
  58. package/libx/odata/middleware/create.js +2 -0
  59. package/libx/odata/middleware/operation.js +8 -2
  60. package/libx/odata/middleware/parse.js +1 -1
  61. package/libx/odata/middleware/stream.js +1 -2
  62. package/libx/odata/middleware/update.js +2 -0
  63. package/libx/odata/parse/afterburner.js +17 -9
  64. package/libx/odata/parse/cqn2odata.js +43 -22
  65. package/libx/odata/parse/grammar.peggy +21 -19
  66. package/libx/odata/parse/parser.js +1 -1
  67. package/libx/odata/utils/metadata.js +8 -2
  68. package/libx/odata/utils/odataBind.js +36 -0
  69. package/libx/outbox/index.js +1 -0
  70. package/libx/rest/middleware/operation.js +9 -8
  71. package/libx/rest/middleware/parse.js +1 -0
  72. package/package.json +3 -3
  73. package/lib/i18n/resources.js +0 -150
@@ -1,11 +1,9 @@
1
- const cds = require('../../cds')
1
+ const cds = require('../../../../lib')
2
2
  let LOG = cds.log('app')
3
- let _event
4
- const PERSISTENCE_TABLE = '@cds.persistence.table'
3
+
5
4
  const { rewriteAsterisks } = require('../../common/utils/rewriteAsterisks')
6
5
 
7
6
  const getError = require('../error')
8
- const { getEntityNameFromDeleteCQN } = require('../utils/cqn')
9
7
 
10
8
  const _setInverseTransition = (mapping, ref, mapped) => {
11
9
  const existing = mapping.get(ref)
@@ -34,7 +32,7 @@ const _inverseTransition = transition => {
34
32
  const ref0 = value.ref[0]
35
33
  if (value.ref.length > 1) {
36
34
  // ignore flattened columns like author.name
37
- if (transition.target.elements[ref0]?.isAssociation) continue
35
+ if (transition.target.elements[ref0].isAssociation) continue
38
36
 
39
37
  const nested = inverseTransition.mapping.get(ref0) || {}
40
38
  if (!nested.transition) nested.transition = { mapping: new Map() }
@@ -57,15 +55,15 @@ const _inverseTransition = transition => {
57
55
  return inverseTransition
58
56
  }
59
57
 
60
- const revertData = (data, transition, service) => {
58
+ const revertData = (data, transition, service, options) => {
61
59
  if (!transition || !transition.mapping.size) return data
62
60
  const inverseTransition = _inverseTransition(transition)
63
61
  return Array.isArray(data)
64
- ? data.map(entry => _newData(entry, inverseTransition, true, service))
65
- : _newData(data, inverseTransition, true, service)
62
+ ? data.map(entry => _newData(entry, inverseTransition, true, service, options))
63
+ : _newData(data, inverseTransition, true, service, options)
66
64
  }
67
65
 
68
- const _newSubData = (val, key, transition, el, inverse, service) => {
66
+ const _newSubData = (val, key, transition, el, inverse, service, options) => {
69
67
  if ((!Array.isArray(val) && typeof val === 'object') || (Array.isArray(val) && val.length !== 0)) {
70
68
  let mapped = transition.mapping.get(key)
71
69
  if (!mapped) {
@@ -74,14 +72,14 @@ const _newSubData = (val, key, transition, el, inverse, service) => {
74
72
  }
75
73
 
76
74
  if (!mapped.transition) {
77
- const subTransition = getTransition(el._target, service)
75
+ const subTransition = getTransition(el._target, service, undefined, options?.event, { abort: options?.abort })
78
76
  mapped.transition = inverse ? _inverseTransition(subTransition) : subTransition
79
77
  }
80
78
 
81
79
  if (Array.isArray(val)) {
82
- return val.map(singleVal => _newData(singleVal, mapped.transition, inverse, service))
80
+ return val.map(singleVal => _newData(singleVal, mapped.transition, inverse, service, options))
83
81
  } else {
84
- return _newData(val, mapped.transition, inverse, service)
82
+ return _newData(val, mapped.transition, inverse, service, options)
85
83
  }
86
84
  }
87
85
  return val //Case of empty array
@@ -105,7 +103,7 @@ const _newNestedData = (queryTarget, newData, ref, value) => {
105
103
  }
106
104
  }
107
105
 
108
- const _newData = (data, transition, inverse, service) => {
106
+ const _newData = (data, transition, inverse, service, options) => {
109
107
  if (data === null) return null
110
108
 
111
109
  // no transition -> nothing to do
@@ -131,12 +129,12 @@ const _newData = (data, transition, inverse, service) => {
131
129
  let value = data[key]
132
130
  if (isAssoc) {
133
131
  if (value || (value === null && service.name === 'db')) {
134
- value = _newSubData(value, key, transition, el, inverse, service)
132
+ value = _newSubData(value, key, transition, el, inverse, service, options)
135
133
  }
136
134
  }
137
135
 
138
136
  if (!isAssoc && mapped.transition) {
139
- value = _newSubData(value, key, transition, el, inverse)
137
+ value = _newSubData(value, key, transition, el, inverse, undefined, options)
140
138
  Object.assign(newData, value)
141
139
  }
142
140
 
@@ -154,14 +152,14 @@ const _newData = (data, transition, inverse, service) => {
154
152
  return newData
155
153
  }
156
154
 
157
- const _newColumns = (columns = [], transition, service, withAlias = false) => {
155
+ const _newColumns = (columns = [], transition, service, withAlias = false, options) => {
158
156
  const newColumns = []
159
157
 
160
158
  columns.forEach(column => {
161
159
  let newColumn
162
160
  if (column.func) {
163
161
  newColumn = { ...column }
164
- newColumn.args = _newColumns(column.args, transition, service, withAlias)
162
+ newColumn.args = _newColumns(column.args, transition, service, withAlias, options)
165
163
  newColumns.push(newColumn)
166
164
  return newColumns
167
165
  }
@@ -200,9 +198,9 @@ const _newColumns = (columns = [], transition, service, withAlias = false) => {
200
198
 
201
199
  // reuse _newColumns with new transition
202
200
  const expandTarget = def._target
203
- const subtransition = getTransition(expandTarget, service)
201
+ const subtransition = getTransition(expandTarget, service, undefined, options.event, { abort: options.abort })
204
202
  mapped.transition = subtransition
205
- newColumn.expand = _newColumns(column.expand, subtransition, service, withAlias)
203
+ newColumn.expand = _newColumns(column.expand, subtransition, service, withAlias, options)
206
204
  }
207
205
 
208
206
  newColumns.push(newColumn)
@@ -269,8 +267,8 @@ const _newWhereRef = (newWhereElement, transition, alias, tableName) => {
269
267
  newWhereElement.ref = newRef
270
268
  }
271
269
 
272
- const _newEntries = (entries = [], transition, service) =>
273
- entries.map(entry => _newData(entry, transition, false, service))
270
+ const _newEntries = (entries = [], transition, service, options) =>
271
+ entries.map(entry => _newData(entry, transition, false, service, options))
274
272
 
275
273
  const _newWhere = (where = [], transition, tableName, alias, isSubselect = false) => {
276
274
  const newWhere = where.map(whereElement => {
@@ -355,7 +353,7 @@ const _rewriteQueryPath = (path, transitions) => {
355
353
  })
356
354
  }
357
355
 
358
- const _newUpdate = (query, transitions, service) => {
356
+ const _newUpdate = (query, transitions, service, options) => {
359
357
  const targetTransition = transitions.at(-1)
360
358
  const targetName = targetTransition.target.name
361
359
  const newUpdate = Object.create(query.UPDATE)
@@ -366,8 +364,8 @@ const _newUpdate = (query, transitions, service) => {
366
364
  ref: _rewriteQueryPath(query.UPDATE.entity, transitions)
367
365
  }
368
366
  : targetName
369
- if (newUpdate.data) newUpdate.data = _newData(newUpdate.data, targetTransition, false, service)
370
- if (newUpdate.with) newUpdate.with = _newData(newUpdate.with, targetTransition, false, service)
367
+ if (newUpdate.data) newUpdate.data = _newData(newUpdate.data, targetTransition, false, service, options)
368
+ if (newUpdate.with) newUpdate.with = _newData(newUpdate.with, targetTransition, false, service, options)
371
369
  if (newUpdate.where) {
372
370
  newUpdate.where = _newWhere(
373
371
  newUpdate.where,
@@ -384,7 +382,7 @@ const _newUpdate = (query, transitions, service) => {
384
382
  return newUpdate
385
383
  }
386
384
 
387
- const _newSelect = (query, transitions, service) => {
385
+ const _newSelect = (query, transitions, service, options) => {
388
386
  const targetTransition = transitions.at(-1)
389
387
  const newSelect = Object.create(query.SELECT)
390
388
  newSelect.from = {
@@ -398,12 +396,21 @@ const _newSelect = (query, transitions, service) => {
398
396
  _4db: service.isDatabaseService,
399
397
  target: targetTransition.queryTarget
400
398
  })
401
- newSelect.columns = _newColumns(newSelect.columns, targetTransition, service, service.kind !== 'app-service')
399
+ newSelect.columns = _newColumns(
400
+ newSelect.columns,
401
+ targetTransition,
402
+ service,
403
+ service.kind !== 'app-service',
404
+ options
405
+ )
402
406
  }
403
407
 
404
- if (newSelect.having) newSelect.having = _newColumns(newSelect.having, targetTransition)
405
- if (newSelect.groupBy) newSelect.groupBy = _newColumns(newSelect.groupBy, targetTransition)
406
- if (newSelect.orderBy) newSelect.orderBy = _newColumns(newSelect.orderBy, targetTransition)
408
+ if (newSelect.having)
409
+ newSelect.having = _newColumns(newSelect.having, targetTransition, undefined, undefined, options)
410
+ if (newSelect.groupBy)
411
+ newSelect.groupBy = _newColumns(newSelect.groupBy, targetTransition, undefined, undefined, options)
412
+ if (newSelect.orderBy)
413
+ newSelect.orderBy = _newColumns(newSelect.orderBy, targetTransition, undefined, undefined, options)
407
414
  if (newSelect.where) {
408
415
  newSelect.where = _newWhere(
409
416
  newSelect.where,
@@ -421,7 +428,7 @@ const _newSelect = (query, transitions, service) => {
421
428
  return newSelect
422
429
  }
423
430
 
424
- const _newInsert = (query, transitions, service) => {
431
+ const _newInsert = (query, transitions, service, options) => {
425
432
  const targetTransition = transitions[transitions.length - 1]
426
433
  const targetName = targetTransition.target.name
427
434
  const newInsert = Object.create(query.INSERT)
@@ -438,7 +445,7 @@ const _newInsert = (query, transitions, service) => {
438
445
  }
439
446
 
440
447
  if (newInsert.columns) newInsert.columns = _newInsertColumns(newInsert.columns, targetTransition)
441
- if (newInsert.entries) newInsert.entries = _newEntries(newInsert.entries, targetTransition, service)
448
+ if (newInsert.entries) newInsert.entries = _newEntries(newInsert.entries, targetTransition, service, options)
442
449
  Object.defineProperty(newInsert, '_transitions', {
443
450
  enumerable: false,
444
451
  value: transitions
@@ -447,7 +454,7 @@ const _newInsert = (query, transitions, service) => {
447
454
  return newInsert
448
455
  }
449
456
 
450
- const _newUpsert = (query, transitions, service) => {
457
+ const _newUpsert = (query, transitions, service, options) => {
451
458
  const targetTransition = transitions[transitions.length - 1]
452
459
  const targetName = targetTransition.target.name
453
460
  const newUpsert = Object.create(query.UPSERT)
@@ -459,7 +466,7 @@ const _newUpsert = (query, transitions, service) => {
459
466
  }
460
467
  : targetName
461
468
  if (newUpsert.columns) newUpsert.columns = _newInsertColumns(newUpsert.columns, targetTransition)
462
- if (newUpsert.entries) newUpsert.entries = _newEntries(newUpsert.entries, targetTransition, service)
469
+ if (newUpsert.entries) newUpsert.entries = _newEntries(newUpsert.entries, targetTransition, service, options)
463
470
  Object.defineProperty(newUpsert, '_transitions', {
464
471
  enumerable: false,
465
472
  value: transitions
@@ -481,12 +488,8 @@ const _newDelete = (query, transitions) => {
481
488
  : targetName
482
489
 
483
490
  if (newDelete.where) {
484
- newDelete.where = _newWhere(
485
- newDelete.where,
486
- targetTransition,
487
- getEntityNameFromDeleteCQN(query),
488
- query.DELETE.from.as
489
- )
491
+ const from = typeof query.DELETE.from === 'string' ? query.DELETE.from : query.DELETE.from.ref[0]
492
+ newDelete.where = _newWhere(newDelete.where, targetTransition, from, query.DELETE.from.as)
490
493
  }
491
494
 
492
495
  Object.defineProperty(newDelete, '_transitions', {
@@ -497,9 +500,6 @@ const _newDelete = (query, transitions) => {
497
500
  return newDelete
498
501
  }
499
502
 
500
- const _isPersistenceTable = target =>
501
- Object.prototype.hasOwnProperty.call(target, PERSISTENCE_TABLE) && target[PERSISTENCE_TABLE]
502
-
503
503
  const _findRenamed = (cqnColumns, column) =>
504
504
  cqnColumns.find(
505
505
  cqnColumn =>
@@ -508,7 +508,7 @@ const _findRenamed = (cqnColumns, column) =>
508
508
  (column.as === cqnColumn.as && Object.prototype.hasOwnProperty.call(cqnColumn, 'val')))
509
509
  )
510
510
 
511
- const _queryColumns = (target, columns = [], persistenceTable = false, force = false) => {
511
+ const _queryColumns = (target, columns = [], isAborted) => {
512
512
  if (!(target && target.query && target.query.SELECT)) return columns
513
513
 
514
514
  const cqnColumns = target.query.SELECT.columns || []
@@ -527,11 +527,7 @@ const _queryColumns = (target, columns = [], persistenceTable = false, force = f
527
527
  if (!renamed.ref || renamed.ref.some(e => typeof e !== 'string') || renamed.xpr) return res
528
528
  if (isTargetAliased && renamed.ref[0] === from.as) renamed.ref.shift()
529
529
 
530
- // If the entity is annotated with the annotation `@cds.persistence.table`
531
- // and elements aliases exist, the aliases must be used as column references.
532
- // The reason is that in this scenario, the cds compiler generate a table
533
- // instead of a view. If forced, skip this.
534
- column.ref = !force && persistenceTable ? [renamed.as] : [...renamed.ref]
530
+ column.ref = isAborted ? [renamed.as] : [...renamed.ref]
535
531
  }
536
532
 
537
533
  res.push(column)
@@ -552,12 +548,7 @@ const _mappedValue = (col, alias) => {
552
548
  return [key, { val: col.val }]
553
549
  }
554
550
 
555
- const getDBTable = target => {
556
- if (target.query && target.query._target && !_isPersistenceTable(target)) {
557
- return getDBTable(target.query._target)
558
- }
559
- return target
560
- }
551
+ const getDBTable = target => cds.ql.resolve.table(target)
561
552
 
562
553
  const _appendForeignKeys = (newColumns, target, columns, { as, ref = [] }) => {
563
554
  const el = target.elements[as] || target.query._target.elements[ref.at(-1)]
@@ -584,55 +575,46 @@ const _appendForeignKeys = (newColumns, target, columns, { as, ref = [] }) => {
584
575
  return newColumns
585
576
  }
586
577
 
587
- const _checkForForbiddenViews = queryTarget => {
578
+ const _checkForForbiddenViews = (queryTarget, event) => {
588
579
  const select = queryTarget && queryTarget.query && queryTarget.query.SELECT
589
580
 
590
581
  if (select) {
591
582
  if (!select.from || select.from.join || select.from.length > 1) {
592
583
  throw getError({
593
584
  code: 501,
594
- message: `${_event || 'INSERT|UPDATE|DELETE'} on views with join and/or union is not supported`,
585
+ message: `${event || 'INSERT|UPDATE|DELETE'} on views with join and/or union is not supported`,
595
586
  target: queryTarget.name
596
587
  })
597
588
  }
598
589
 
599
590
  if (select.where) {
600
591
  LOG._debug &&
601
- LOG.debug(`Ignoring where clause during ${_event || 'INSERT|UPDATE|DELETE'} on view "${queryTarget.name}".`)
592
+ LOG.debug(`Ignoring where clause during ${event || 'INSERT|UPDATE|DELETE'} on view "${queryTarget.name}".`)
602
593
  }
603
594
  }
604
595
  }
605
596
 
606
- const _getTransitionData = (target, columns, service, skipForbiddenViewCheck) => {
597
+ const _getTransitionData = (target, columns, service, options) => {
598
+ let { abort, skipForbiddenViewCheck, event } = options
599
+ // REVISIT revert after cds-dbs pr
600
+ if (!abort) abort = cds.ql.resolve.abortDB
607
601
  // REVISIT: Find less param polluting way to skip forbidden view check for reads
608
- if (!skipForbiddenViewCheck) _checkForForbiddenViews(target)
609
- const targetStartsWithSrvName = service.definition?.name && target.name.startsWith(`${service.definition.name}.`)
610
- const persistenceTable = _isPersistenceTable(target)
611
- const isDatabaseService = service.isDatabaseService
612
- columns = _queryColumns(target, columns, persistenceTable, !isDatabaseService && !targetStartsWithSrvName)
613
-
614
- // REVISIT: Change once we expose database service
615
- if (persistenceTable && isDatabaseService) {
616
- return { target, transitionColumns: columns }
617
- }
602
+ if (!skipForbiddenViewCheck) _checkForForbiddenViews(target, event)
603
+ const isAborted = abort(target)
604
+ columns = _queryColumns(target, columns, isAborted)
618
605
 
619
- // stop projection resolving if it starts with the service name prefix
620
- if (!isDatabaseService && targetStartsWithSrvName) {
621
- return { target, transitionColumns: columns }
622
- }
606
+ if (isAborted) return { target, transitionColumns: columns }
623
607
 
624
- // continue projection resolving if the target is a projection
625
- if (target.query && target.query._target) {
608
+ if (!target.query?._target) {
609
+ // for cross service in x4 and DRAFT.DraftAdministrativeData we cannot abort properly
610
+ // therefore return last resolved target
611
+ if (cds.env.features.restrict_service_scope === false) return { target, transitionColumns: columns }
612
+ return undefined
613
+ } else {
626
614
  const newTarget = target.query._target
627
-
628
- if (isDatabaseService || !(service.definition?.name && newTarget.name.startsWith(`${service.definition.name}.`))) {
629
- return _getTransitionData(newTarget, columns, service, skipForbiddenViewCheck)
630
- }
631
-
632
- return { target: newTarget, transitionColumns: columns }
615
+ // continue projection resolving for projections
616
+ return _getTransitionData(newTarget, columns, service, options)
633
617
  }
634
-
635
- return { target, transitionColumns: columns }
636
618
  }
637
619
 
638
620
  /**
@@ -642,14 +624,19 @@ const _getTransitionData = (target, columns, service, skipForbiddenViewCheck) =>
642
624
  * @param service
643
625
  * @param skipForbiddenViewCheck
644
626
  */
645
- const getTransition = (queryTarget, service, skipForbiddenViewCheck, event) => {
646
- if (event) _event = event
627
+ const getTransition = (queryTarget, service, skipForbiddenViewCheck, event, options) => {
647
628
  // Never resolve unknown targets (e.g. for drafts)
648
629
  if (!queryTarget) {
649
630
  return { target: queryTarget, queryTarget, mapping: new Map() }
650
631
  }
651
632
 
652
- const { target: _target, transitionColumns } = _getTransitionData(queryTarget, [], service, skipForbiddenViewCheck)
633
+ const transitionData = _getTransitionData(queryTarget, [], service, {
634
+ skipForbiddenViewCheck,
635
+ event,
636
+ abort: options?.abort
637
+ })
638
+ if (!transitionData) return undefined
639
+ const { target: _target, transitionColumns } = transitionData
653
640
  const query = queryTarget.query
654
641
  const alias = query && query.SELECT && query.SELECT.from && query.SELECT.from.as
655
642
  const mappedColumns = transitionColumns.map(column => _mappedValue(column, alias))
@@ -657,11 +644,15 @@ const getTransition = (queryTarget, service, skipForbiddenViewCheck, event) => {
657
644
  return { target: _target, queryTarget, mapping }
658
645
  }
659
646
 
660
- const _entityTransitionsForTarget = (from, model, service) => {
647
+ const _entityTransitionsForTarget = (from, model, service, options) => {
661
648
  let previousEntity
662
649
 
663
650
  if (typeof from === 'string') {
664
- return model.definitions[from] && [getTransition(model.definitions[from], service)]
651
+ return (
652
+ model.definitions[from] && [
653
+ getTransition(model.definitions[from], service, undefined, options.event, { abort: options.abort })
654
+ ]
655
+ )
665
656
  }
666
657
 
667
658
  return from.ref.map((f, i) => {
@@ -671,7 +662,7 @@ const _entityTransitionsForTarget = (from, model, service) => {
671
662
  const entity = model.definitions[element]
672
663
  if (entity) {
673
664
  previousEntity = entity
674
- return getTransition(entity, service)
665
+ return getTransition(entity, service, undefined, options.event, { abort: options.abort })
675
666
  }
676
667
  }
677
668
 
@@ -680,7 +671,7 @@ const _entityTransitionsForTarget = (from, model, service) => {
680
671
  if (entity) {
681
672
  // > assoc
682
673
  previousEntity = entity
683
- return getTransition(entity, service)
674
+ return getTransition(entity, service, undefined, options.event, { abort: options.abort })
684
675
  }
685
676
 
686
677
  // > struct
@@ -694,7 +685,8 @@ const _entityTransitionsForTarget = (from, model, service) => {
694
685
  })
695
686
  }
696
687
 
697
- const _newQuery = (query, event, model, service) => {
688
+ const _newQuery = (query, model, service, options) => {
689
+ const { event } = options
698
690
  const [_prop, _func] = {
699
691
  SELECT: ['from', _newSelect],
700
692
  INSERT: ['into', _newInsert],
@@ -703,30 +695,32 @@ const _newQuery = (query, event, model, service) => {
703
695
  DELETE: ['from', _newDelete]
704
696
  }[event]
705
697
  const newQuery = Object.create(query)
706
- const transitions = _entityTransitionsForTarget(query[event][_prop], model, service)
707
- newQuery[event] = (transitions?.[0] && _func(newQuery, transitions, service)) || { ...query[event] }
698
+ const transitions = _entityTransitionsForTarget(query[event][_prop], model, service, options)
699
+ if (!service.isDatabaseService && cds.env.features.restrict_service_scope !== false && transitions.some(t => !t))
700
+ return undefined
701
+ newQuery[event] = (transitions?.[0] && _func(newQuery, transitions, service, options)) || { ...query[event] }
708
702
  return newQuery
709
703
  }
710
704
 
711
- const resolveView = (query, model, service) => {
705
+ const resolveView = (query, model, service, abort) => {
712
706
  // swap logger
713
707
  const _LOG = LOG
714
708
  LOG = cds.log(service.kind) // REVISIT: Avoid obtaining loggers per request!
715
709
 
716
710
  // If the query is a projection, one must follow it
717
711
  // to let the underlying service know its true entity.
718
- if (query.kind) _event = query.kind
719
- else if (query.SELECT) _event = 'SELECT'
720
- else if (query.INSERT) _event = 'INSERT'
721
- else if (query.UPSERT) _event = 'UPSERT'
722
- else if (query.UPDATE) _event = 'UPDATE'
723
- else if (query.DELETE) _event = 'DELETE'
712
+ let event
713
+ if (query.kind) event = query.kind
714
+ else if (query.SELECT) event = 'SELECT'
715
+ else if (query.INSERT) event = 'INSERT'
716
+ else if (query.UPSERT) event = 'UPSERT'
717
+ else if (query.UPDATE) event = 'UPDATE'
718
+ else if (query.DELETE) event = 'DELETE'
724
719
 
725
- const newQuery = _newQuery(query, _event, model, service)
720
+ const newQuery = _newQuery(query, model, service, { abort, event: event })
726
721
 
727
- // restore logger and clear _event
722
+ // restore logger
728
723
  LOG = _LOG
729
- _event = undefined
730
724
 
731
725
  return newQuery
732
726
  }
@@ -2,7 +2,7 @@ const { getNavigationIfStruct } = require('./structured')
2
2
  const getColumns = require('../../db/utils/columns')
3
3
  const { ensureNoDraftsSuffix } = require('./draft')
4
4
  const { getEntityNameFromCQN } = require('./entityFromCqn')
5
- const cds = require('../../cds')
5
+ const cds = require('../../../../lib')
6
6
 
7
7
  const isAsteriskColumn = col => col === '*' || (col.ref && col.ref[0] === '*' && !col.expand)
8
8
 
@@ -2,7 +2,6 @@ const cds = require('../../cds')
2
2
  const LOG = cds.log('odata')
3
3
  const { SELECT } = cds.ql
4
4
  const { deepCopy } = require('./copy')
5
- const { getTransition } = require('./resolveView')
6
5
 
7
6
  const _getStreamProperties = (req, query) => {
8
7
  // new odata parser sets streaming property in SELECT.from
@@ -29,7 +28,7 @@ const _getStreamProperties = (req, query) => {
29
28
  LOG.warn(
30
29
  `@Core.MediaType in entity "${req.target.name}" points to property "${contentTypeProperty}" which was renamed or is not part of the projection. You must update the annotation value.`
31
30
  )
32
- const mapping = getTransition(req.target, cds.db).mapping
31
+ const mapping = cds.ql.resolve.transitions(req.query, cds.db).mapping
33
32
  const key = [...mapping.entries()].find(({ 1: val }) => val.ref[0] === contentTypeProperty)
34
33
  contentTypeProperty = key && key.length && key[0]
35
34
  }
@@ -52,7 +51,7 @@ const _getStreamProperties = (req, query) => {
52
51
  LOG.warn(
53
52
  `@Core.ContentDisposition.Filename in entity "${req.target.name}" points to property "${contentDispositionProperty}" which was renamed or is not part of the projection. You must update the annotation value.`
54
53
  )
55
- const mapping = getTransition(req.target, cds.db).mapping
54
+ const mapping = cds.ql.resolve.transitions(req.query, cds.db).mapping
56
55
  const key = [...mapping.entries()].find(({ 1: val }) => val.ref[0] === contentDispositionProperty)
57
56
  contentDispositionProperty = key && key.length && key[0]
58
57
  }
@@ -1,4 +1,4 @@
1
- const cds = require('../../cds')
1
+ const cds = require('../../../../lib')
2
2
  const resolveStructured = require('../../common/utils/resolveStructured')
3
3
 
4
4
  const _isStreamProperty = element => {
@@ -2,8 +2,6 @@ const { Readable, PassThrough } = require('stream')
2
2
  const cds = require('../cds'),
3
3
  { Object_keys } = cds.utils
4
4
 
5
- const { getTransition } = require('../common/utils/resolveView')
6
-
7
5
  const { getPageSize, commonGenericPaging } = require('../common/generic/paging')
8
6
  const { handler: commonGenericSorting } = require('../common/generic/sorting')
9
7
  const { addEtagColumns } = require('../common/utils/etag')
@@ -639,6 +637,9 @@ cds.ApplicationService.prototype.handle = async function (req) {
639
637
  'location',
640
638
  '../' + calculateLocationHeader(req.target, this, read_result || { ...res, IsActiveEntity: true })
641
639
  )
640
+
641
+ if (read_result == null) req.res.status(204)
642
+
642
643
  return read_result
643
644
  }
644
645
 
@@ -804,7 +805,7 @@ const Read = {
804
805
  // DraftAdministrativeData is only accessible via drafts
805
806
  if (_isCount(query)) return run(query)
806
807
  if (query._target.name.endsWith('.DraftAdministrativeData')) {
807
- if (query.SELECT.from.ref?.length === 1) throw new Error('Invalid draft request') // only via drafts
808
+ if (query.SELECT.from.ref?.length === 1) throw cds.error('INVALID_DRAFT_REQUEST', { statusCode: 400 }) // only via drafts
808
809
  return run(query._drafts)
809
810
  }
810
811
  if (!query._target._isDraftEnabled) return run(query)
@@ -852,7 +853,7 @@ const Read = {
852
853
  LOG.debug('List Editing Status: Unchanged')
853
854
 
854
855
  const draftsQuery = query._drafts
855
- if (!draftsQuery) throw new Error('Invalid draft request')
856
+ if (!draftsQuery) throw cds.error('INVALID_DRAFT_REQUEST', { statusCode: 400 }) // only via drafts
856
857
  draftsQuery.SELECT.count = undefined
857
858
  draftsQuery.SELECT.orderBy = undefined
858
859
  draftsQuery.SELECT.limit = null
@@ -868,7 +869,7 @@ const Read = {
868
869
  ownDrafts: async function (run, query) {
869
870
  LOG.debug('List Editing Status: Own Draft')
870
871
 
871
- if (!query._drafts) throw new Error('Invalid draft request')
872
+ if (!query._drafts) throw cds.error('INVALID_DRAFT_REQUEST', { statusCode: 400 }) // only via drafts
872
873
 
873
874
  // read active from draft
874
875
  if (!query._drafts._target?.name.endsWith('.drafts')) {
@@ -1069,7 +1070,7 @@ const Read = {
1069
1070
 
1070
1071
  activesFromDrafts: async function (run, query, { isLocked = true }) {
1071
1072
  const draftsQuery = query._drafts
1072
- if (!draftsQuery) throw new Error('Invalid draft request')
1073
+ if (!draftsQuery) throw cds.error('INVALID_DRAFT_REQUEST', { statusCode: 400 }) // only via drafts
1073
1074
 
1074
1075
  const additionalCols = draftsQuery.SELECT.columns
1075
1076
  ? draftsQuery.SELECT.columns.filter(
@@ -1668,7 +1669,7 @@ async function onEdit(req) {
1668
1669
  res[key] = keyData[key]
1669
1670
  return res
1670
1671
  }, {})
1671
- const transition = getTransition(req.target, cds.db)
1672
+ const transition = cds.ql.resolve.transitions(req.query, cds.db)
1672
1673
 
1673
1674
  // gets the underlying target entity, as record locking can't be
1674
1675
  // applied to localized views
@@ -1781,6 +1782,9 @@ async function onEdit(req) {
1781
1782
  'location',
1782
1783
  '../' + calculateLocationHeader(req.target, this, read_result || { ...res, IsActiveEntity: false })
1783
1784
  )
1785
+
1786
+ if (read_result == null) req.res.status(204)
1787
+
1784
1788
  return read_result
1785
1789
  } else {
1786
1790
  return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
@@ -51,7 +51,7 @@ class AMQPWebhookMessaging extends MessagingService {
51
51
  if (!msg._) msg._ = {}
52
52
  msg._.topic = _topic
53
53
  try {
54
- await this.tx({ user: cds.User.privileged, tenant: msg.tenant, _: msg._ }, tx => tx.emit(msg))
54
+ await this.processInboundMsg({ tenant: msg.tenant, _: msg._ }, msg)
55
55
  done()
56
56
  } catch (e) {
57
57
  // In case of AMQP and Solace, the `failed` callback must be called
@@ -22,8 +22,7 @@ const normalizeIncomingMessage = message => {
22
22
 
23
23
  return {
24
24
  data,
25
- headers,
26
- inbound: true
25
+ headers
27
26
  }
28
27
  }
29
28
 
@@ -54,12 +54,12 @@ class FileBasedMessaging extends MessagingService {
54
54
  if (this.subscribedTopics.has(topic)) {
55
55
  const event = this.subscribedTopics.get(topic)
56
56
  if (!event) return
57
- this.tx(tx =>
58
- tx.emit({ event, ...json, inbound: true }).catch(e => {
59
- e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
60
- this.LOG.error(e)
61
- })
62
- )
57
+ try {
58
+ await this.processInboundMsg({}, { event, ...json })
59
+ } catch (e) {
60
+ e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
61
+ this.LOG.error(e)
62
+ }
63
63
  } else other.push(each + '\n')
64
64
  }
65
65
  } catch {
@@ -13,8 +13,7 @@ class KafkaService extends cds.MessagingService {
13
13
 
14
14
  this._cachedKeyFns = new Map()
15
15
 
16
- // TODO: Make configurable
17
- this.appId = appId()
16
+ this.consumerGroup = this.options.consumerGroup || appId() + '/' + this.options.topic
18
17
 
19
18
  if (!this.options.local && !this.options.credentials) {
20
19
  throw new Error(
@@ -95,7 +94,7 @@ class KafkaService extends cds.MessagingService {
95
94
  }
96
95
 
97
96
  async startListening() {
98
- const consumer = this.client.consumer({ groupId: this.appId })
97
+ const consumer = this.client.consumer({ groupId: this.consumerGroup })
99
98
  await consumer.connect()
100
99
 
101
100
  // In the future, we might allow to support the annotation @kafka.topic
@@ -132,7 +131,7 @@ class KafkaService extends cds.MessagingService {
132
131
  msg.tenant = raw.message.headers['x-sap-cap-tenant-id']
133
132
  if (!msg.event) return
134
133
 
135
- await this.tx({ user: cds.User.privileged, tenant: msg.tenant }, tx => tx.emit(msg))
134
+ await this.processInboundMsg({ tenant: msg.tenant }, msg)
136
135
  } catch (e) {
137
136
  if (e.code === 'NO_HANDLER_FOUND') return // consume
138
137
  this.LOG.error('ERROR occured in asynchronous event processing:', e)
@@ -170,8 +169,7 @@ function _normalizeIncomingMessage(message) {
170
169
 
171
170
  return {
172
171
  data,
173
- headers,
174
- inbound: true
172
+ headers
175
173
  }
176
174
  }
177
175
 
@@ -216,7 +214,7 @@ async function _getConfig(srv) {
216
214
  const brokers = allBrokers.split(',')
217
215
 
218
216
  return {
219
- clientId: srv.appId,
217
+ clientId: srv.consumerGroup,
220
218
  // logLevel: 4,
221
219
  connectionTimeout: 15000,
222
220
  authenticationTimeout: 15000,