@sap/cds 9.4.5 → 9.5.2

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 (55) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/_i18n/messages_en_US_saptrc.properties +1 -1
  3. package/common.cds +5 -2
  4. package/lib/compile/cds-compile.js +1 -0
  5. package/lib/compile/for/assert.js +64 -0
  6. package/lib/compile/for/flows.js +194 -58
  7. package/lib/compile/for/lean_drafts.js +75 -7
  8. package/lib/compile/parse.js +1 -1
  9. package/lib/compile/to/csn.js +6 -2
  10. package/lib/compile/to/edm.js +1 -1
  11. package/lib/compile/to/yaml.js +8 -1
  12. package/lib/dbs/cds-deploy.js +2 -2
  13. package/lib/env/cds-env.js +14 -4
  14. package/lib/env/defaults.js +6 -1
  15. package/lib/i18n/localize.js +1 -1
  16. package/lib/index.js +7 -7
  17. package/lib/req/event.js +4 -0
  18. package/lib/req/validate.js +3 -0
  19. package/lib/srv/cds.Service.js +2 -1
  20. package/lib/srv/middlewares/auth/ias-auth.js +5 -7
  21. package/lib/srv/middlewares/auth/index.js +1 -1
  22. package/lib/srv/protocols/index.js +7 -6
  23. package/lib/srv/srv-handlers.js +7 -0
  24. package/libx/_runtime/common/Service.js +5 -1
  25. package/libx/_runtime/common/constants/events.js +1 -0
  26. package/libx/_runtime/common/generic/assert.js +236 -0
  27. package/libx/_runtime/common/generic/flows.js +168 -108
  28. package/libx/_runtime/common/utils/cqn.js +0 -24
  29. package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
  30. package/libx/_runtime/common/utils/resolveView.js +8 -2
  31. package/libx/_runtime/common/utils/templateProcessor.js +10 -1
  32. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
  33. package/libx/_runtime/fiori/lean-draft.js +511 -379
  34. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
  35. package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
  36. package/libx/_runtime/remote/Service.js +4 -5
  37. package/libx/_runtime/ucl/Service.js +111 -15
  38. package/libx/common/utils/streaming.js +1 -1
  39. package/libx/odata/middleware/batch.js +8 -6
  40. package/libx/odata/middleware/create.js +2 -2
  41. package/libx/odata/middleware/delete.js +2 -2
  42. package/libx/odata/middleware/metadata.js +18 -11
  43. package/libx/odata/middleware/read.js +2 -2
  44. package/libx/odata/middleware/service-document.js +1 -1
  45. package/libx/odata/middleware/update.js +1 -1
  46. package/libx/odata/parse/afterburner.js +24 -25
  47. package/libx/odata/parse/cqn2odata.js +2 -6
  48. package/libx/odata/parse/grammar.peggy +90 -12
  49. package/libx/odata/parse/parser.js +1 -1
  50. package/libx/odata/utils/index.js +2 -2
  51. package/libx/odata/utils/readAfterWrite.js +2 -0
  52. package/libx/queue/TaskRunner.js +26 -1
  53. package/libx/queue/index.js +11 -1
  54. package/package.json +1 -1
  55. package/srv/ucl-service.cds +2 -0
@@ -150,19 +150,19 @@ function _convertVal(value, element) {
150
150
  case 'cds.Int32':
151
151
  if (!/^-?\+?\d+$/.test(value)) {
152
152
  const msg = `Element "${element.name}" does not contain a valid Integer`
153
- throw Object.assign(new Error(msg), { statusCode: 400 })
153
+ cds.error({ status: 400, message: msg })
154
154
  }
155
155
 
156
156
  // eslint-disable-next-line no-case-declarations
157
157
  const n = Number(value)
158
158
  if (!Number.isSafeInteger(n)) {
159
159
  const msg = `Element "${element.name}" does not contain a valid Integer`
160
- throw Object.assign(new Error(msg), { statusCode: 400 })
160
+ cds.error({ status: 400, message: msg })
161
161
  }
162
162
 
163
163
  if (element._type === 'cds.UInt8' && n < 0) {
164
164
  const msg = `Element "${element.name}" does not contain a valid positive Integer`
165
- throw Object.assign(new Error(msg), { statusCode: 400 })
165
+ cds.error({ status: 400, message: msg })
166
166
  }
167
167
 
168
168
  return n
@@ -191,7 +191,7 @@ function _convertVal(value, element) {
191
191
  case 'cds.UUID':
192
192
  if (!RELAXED_UUID_REGEX.test(value)) {
193
193
  const msg = `Element "${element.name}" does not contain a valid UUID`
194
- throw Object.assign(new Error(msg), { statusCode: 400 })
194
+ cds.error({ status: 400, message: msg })
195
195
  }
196
196
  return value
197
197
 
@@ -261,7 +261,7 @@ function _handleCollectionBoundActions(current, ref, i, namespace, one) {
261
261
  const msg = `${action.kind.at(0).toUpperCase() + action.kind.slice(1)} "${
262
262
  action.name
263
263
  }" must be called on a collection of ${current.name}`
264
- throw Object.assign(new Error(msg), { statusCode: 400 })
264
+ cds.error({ status: 400, message: msg })
265
265
  }
266
266
 
267
267
  if (incompleteKeys) {
@@ -269,7 +269,7 @@ function _handleCollectionBoundActions(current, ref, i, namespace, one) {
269
269
  const msg = `${action.kind.at(0).toUpperCase() + action.kind.slice(1)} "${
270
270
  action.name
271
271
  }" must be called on a single instance of ${current.name}`
272
- throw Object.assign(new Error(msg), { statusCode: 400 })
272
+ cds.error({ status: 400, message: msg })
273
273
  }
274
274
 
275
275
  incompleteKeys = false
@@ -336,7 +336,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
336
336
 
337
337
  ref[i] = null
338
338
  ref[i - keyCount] = base
339
- incompleteKeys = keyCount < keys.length
339
+ incompleteKeys = keyCount < keys.filter(k => k !== 'IsActiveEntity').length
340
340
  } else {
341
341
  // > entity or property (incl. nested) or navigation or action or function
342
342
  keys = null
@@ -362,7 +362,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
362
362
  } else {
363
363
  // parentheses are missing
364
364
  const msg = `Invalid call to "${current.name}". Parentheses are missing`
365
- throw cds.error(msg, { code: '400', statusCode: 400 })
365
+ cds.error({ status: 400, message: msg })
366
366
  }
367
367
 
368
368
  _addDefaultParams(ref[i], current)
@@ -383,7 +383,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
383
383
  if (ref[i + 1] !== 'Set') {
384
384
  // /Set is missing
385
385
  const msg = `Invalid call to "${current.name}". You need to navigate to Set`
386
- throw cds.error(msg, { code: '400', statusCode: 400 })
386
+ cds.error({ status: 400, message: msg })
387
387
  }
388
388
 
389
389
  ref[++i] = null
@@ -405,19 +405,19 @@ function _processSegments(from, model, namespace, cqn, protocol) {
405
405
 
406
406
  if (keyCount === 0 && !Object.keys(params).length && whereRef.length === 1) {
407
407
  const msg = `Entity "${current.name}" can not be accessed by key.`
408
- throw Object.assign(new Error(msg), { statusCode: 400 })
408
+ cds.error({ status: 400, message: msg })
409
409
  }
410
410
  }
411
411
  } else if ({ action: 1, function: 1 }[current.kind]) {
412
412
  // > action or function
413
413
  if (current.kind === 'action' && ref && ref.at(-1)?.where?.length === 0) {
414
414
  const msg = `Parentheses are not allowed for action calls.`
415
- throw Object.assign(new Error(msg), { statusCode: 400 })
415
+ cds.error({ status: 400, message: msg })
416
416
  }
417
417
 
418
418
  if (i !== ref.length - 1) {
419
419
  const msg = `${i ? 'Bound' : 'Unbound'} ${current.kind}s are only supported as the last path segment`
420
- throw Object.assign(new Error(msg), { statusCode: 400 })
420
+ cds.error({ status: 400, message: msg })
421
421
  }
422
422
 
423
423
  ref[i] = { operation: current.name }
@@ -503,7 +503,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
503
503
  const propRef = ref.slice(i)
504
504
  if (propRef[0].where?.length === 0) {
505
505
  const msg = 'Parentheses are not allowed when addressing properties.'
506
- throw Object.assign(new Error(msg), { statusCode: 400 })
506
+ cds.error({ status: 400, message: msg })
507
507
  }
508
508
  cqn.SELECT.columns.push({ ref: propRef })
509
509
 
@@ -527,7 +527,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
527
527
  const msg = `Entity "${current.name}" has ${keysOf(current).length} keys. Only ${keyCount} ${
528
528
  keyCount === 1 ? 'was' : 'were'
529
529
  } provided.`
530
- throw Object.assign(new Error(msg), { statusCode: 400 })
530
+ cds.error({ status: 400, message: msg })
531
531
  }
532
532
 
533
533
  // remove all nulled refs
@@ -607,9 +607,9 @@ function _processColumns(cqn, target, protocol) {
607
607
 
608
608
  // Error if groupBy is present but no columns are selected
609
609
  if (columns && !columns.length && cqn.SELECT.groupBy) {
610
- throw cds.error('Explicit select must include at least one column available in the result set of groupby', {
611
- code: '400',
612
- statusCode: 400
610
+ cds.error({
611
+ status: 400,
612
+ message: 'Explicit select must include at least one column available in the result set of groupby'
613
613
  })
614
614
  }
615
615
 
@@ -695,9 +695,8 @@ function _processColumns(cqn, target, protocol) {
695
695
  if (processedColumn.func === null) {
696
696
  processedColumn.func = aggregatedElement?.[AGGR_DFLT]?.['#']?.toLowerCase()
697
697
  if (!customAggregate)
698
- throw cds.error(`Result type for custom aggregation of property "${aggregatedPropertyName}" not found`)
699
- if (!processedColumn.func)
700
- throw cds.error(`Default aggregation for property "${aggregatedPropertyName}" not found`)
698
+ cds.error(`Result type for custom aggregation of property "${aggregatedPropertyName}" not found`)
699
+ if (!processedColumn.func) cds.error(`Default aggregation for property "${aggregatedPropertyName}" not found`)
701
700
  if (processedColumn.func === 'count_distinct') processedColumn.func = 'countdistinct'
702
701
  }
703
702
 
@@ -705,7 +704,7 @@ function _processColumns(cqn, target, protocol) {
705
704
  const semanticsAmountElementName = aggregatedElement?.[SMTCS_AMT_CC] ?? aggregatedElement?.[SMTCS_AMT_UOM]
706
705
  if (!semanticsAmountElementName) continue
707
706
  if (!target.elements[semanticsAmountElementName])
708
- throw cds.error(`Referenced semantics amount element not found: ${semanticsAmountElementName}`)
707
+ cds.error(`Referenced semantics amount element not found: ${semanticsAmountElementName}`)
709
708
  const semanticsAmountElementRef = [...prefixRef, semanticsAmountElementName]
710
709
 
711
710
  columns[i] = {
@@ -737,7 +736,7 @@ const _checkAllKeysProvided = (params, entity) => {
737
736
  if (isView) {
738
737
  // view with params
739
738
  if (params === undefined) {
740
- throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, { code: '400', statusCode: 400 })
739
+ cds.error({ status: 400, message: `Invalid call to "${entity.name}". You need to navigate to Set` })
741
740
  }
742
741
 
743
742
  keysOfEntity = Object.keys(entity.params)
@@ -747,7 +746,7 @@ const _checkAllKeysProvided = (params, entity) => {
747
746
 
748
747
  if (!keysOfEntity) return
749
748
  for (const keyOfEntity of keysOfEntity) {
750
- if (!(keyOfEntity in params)) {
749
+ if (keyOfEntity !== 'IsActiveEntity' && !(keyOfEntity in params)) {
751
750
  if (isView && entity.params[keyOfEntity].default) {
752
751
  // will be added later?
753
752
  continue
@@ -755,7 +754,7 @@ const _checkAllKeysProvided = (params, entity) => {
755
754
 
756
755
  // prettier-ignore
757
756
  const msg = `${isView ? 'Parameter' : 'Key'} "${keyOfEntity}" is missing for ${isView ? 'view' : 'entity'} "${entity.name}"`
758
- throw Object.assign(new Error(msg), { statusCode: 400 })
757
+ cds.error({ status: 400, message: msg })
759
758
  }
760
759
  }
761
760
  }
@@ -764,7 +763,7 @@ const _doesNotExistError = (isExpand, refName, targetName, targetKind) => {
764
763
  const msg = isExpand
765
764
  ? `Navigation property "${refName}" does not exist in "${targetName}"`
766
765
  : `Property "${refName}" does not exist in ${targetKind === 'type' ? 'type ' : ''}"${targetName}"`
767
- throw Object.assign(new Error(msg), { statusCode: 400 })
766
+ cds.error({ status: 400, message: msg })
768
767
  }
769
768
 
770
769
  const _get_service_of = element => {
@@ -210,7 +210,7 @@ function _xpr(expr, target, kind, isLambda, navPrefix = []) {
210
210
  res.push(OPERATORS[cur] || cur.toLowerCase())
211
211
  }
212
212
  } else {
213
- const ref = expr[i - 2]
213
+ const ref = expr[i - 1] in OPERATORS ? expr[i - 2] : expr[i + 1] in OPERATORS ? expr[i + 2] : null
214
214
  const formatted = _format(
215
215
  cur,
216
216
  ref?.ref && (ref.ref.length ? ref.ref : ref.ref[0]),
@@ -281,7 +281,7 @@ function _getQueryTarget(entity, propOrEntity, model) {
281
281
 
282
282
  const _params = (args, kind, target) => {
283
283
  if (!args) {
284
- throw cds.error(`Invalid call to "${target.name}". You need to navigate to Set`, { code: '400', statusCode: 400 })
284
+ cds.error({ status: 400, message: `Invalid call to "${target.name}". You need to navigate to Set` })
285
285
  }
286
286
  const params = Object.keys(args)
287
287
  if (params.length !== Object.keys(target.params).length) {
@@ -298,10 +298,6 @@ const _params = (args, kind, target) => {
298
298
  }
299
299
 
300
300
  function _from(from, kind, model) {
301
- if (typeof from === 'string') {
302
- return { url: _entityUrl(from), queryTarget: model && model.definitions[from] }
303
- }
304
-
305
301
  let ref = getProp(from, 'ref')
306
302
  ref = (Array.isArray(ref) && ref) || [ref]
307
303
 
@@ -515,8 +515,58 @@
515
515
  })
516
516
  return isAliased
517
517
  }
518
+
519
+ const _replaceComputedProperties = (columns, col) => {
520
+ for (let i = 0; i < columns.length; i++) {
521
+ if (columns[i].ref && columns[i].ref.at(-1) === col.as && (col.xpr || col.ref || col.val)) {
522
+ columns[i] = col
523
+ return true;
524
+ }
525
+ }
526
+ return false;
527
+ }
528
+
529
+ const _checkComputedPropsUsage = (elements, computedAliases, context) => {
530
+ if (!elements || !computedAliases.length) return
531
+
532
+ const _checkElement = element => {
533
+ if (element.ref && computedAliases.includes(element.ref.at(-1))) {
534
+ const err = new Error(`Computed property "${element.ref.at(-1)}" cannot be used in ${context}. Computed properties are only supported in $select`)
535
+ err.statusCode = 501
536
+ throw err
537
+ }
538
+ if (Array.isArray(element.xpr)) element.xpr.forEach(_checkElement)
539
+ if (Array.isArray(element.args)) element.args.forEach(_checkElement)
540
+ }
541
+
542
+ if (Array.isArray(elements)) elements.forEach(_checkElement)
543
+ else _checkElement(elements)
544
+ }
545
+
546
+ const _validateComputedPropsUsage = (SELECT) => {
547
+ const computedAliases = SELECT.compute.map(col => col.as)
548
+
549
+ if (SELECT.where) _checkComputedPropsUsage(SELECT.where, computedAliases, '$filter')
550
+ if (SELECT.orderBy) _checkComputedPropsUsage(SELECT.orderBy, computedAliases, '$orderby')
551
+ }
552
+
553
+ const _processComputedProps = (SELECT) => {
554
+ _validateComputedPropsUsage(SELECT)
555
+
556
+ for (const col of SELECT.compute) {
557
+ if (SELECT.columns) {
558
+ const propFound = _replaceComputedProperties(SELECT.columns, col)
559
+ if (!propFound) SELECT.columns.push(col)
560
+ } else {
561
+ SELECT.columns = ['*']
562
+ SELECT.columns.push(col)
563
+ }
564
+ }
565
+ delete SELECT.compute
566
+ }
518
567
  }
519
568
 
569
+
520
570
  // ---------- Entity Paths ---------------
521
571
 
522
572
  ODataRelativeURI // Note: case-sensitive!
@@ -551,6 +601,8 @@
551
601
  SELECT.__countAggregated = true
552
602
  if(SELECT.apply)
553
603
  return _handleApply(SELECT, SELECT.apply, onlyColumnsFromExpand)
604
+ if(SELECT.compute)
605
+ _processComputedProps(SELECT)
554
606
  return { SELECT }
555
607
  }
556
608
 
@@ -640,10 +692,11 @@
640
692
  aliasedParamEqualsValOrPrefixParam /
641
693
  deltaToken
642
694
  // @OData spec for $expand:
643
- // "Allowed system query options are $filter, $select, $orderby, $skip, $top, $count, $search, $expand and
695
+ // "Allowed system query options are $filter, $select, $compute, $orderby, $skip, $top, $count, $search, $expand and
644
696
  // $apply (https://go.sap.corp/0jzs)."
645
697
  ExpandOption =
646
698
  "$select=" o select ( COMMA select )* /
699
+ "$compute=" o compute ( COMMA compute )* /
647
700
  "$expand=" o expand ( COMMA expand )* expandCount? /
648
701
  "$filter=" o f:filter { SELECT.where = f } /
649
702
  "$orderby=" o o:orderby ( COMMA o2:orderby{_setOrderBy(o2)} )* {_setOrderBy(o,true)} /
@@ -748,6 +801,14 @@
748
801
  = val:$( [^;)]+ ) { return [{ val }] }
749
802
  / o // Do not add search property for space only
750
803
 
804
+ compute
805
+ = expr:mathCalc as:asAlias {
806
+ SELECT.compute = Array.isArray(SELECT.compute) ? SELECT.compute : []
807
+ const comp = expr.xpr ? { xpr: expr.xpr, as } : { ...expr, as }
808
+ SELECT.compute.push(comp)
809
+ return comp
810
+ }
811
+
751
812
  filter
752
813
  = p:where_clause { return p }
753
814
 
@@ -877,6 +938,33 @@
877
938
  //
878
939
  // ---------- Expressions ------------
879
940
 
941
+ mathCalc
942
+ = head:mathOperand tail:(o op:("add" / "sub" / "mul" / "divby" / "div" ) o operand:mathOperand { return { op, operand } })* {
943
+ const opMap = { sub: '-', add: '+', mul: '*', divby: '/', div: '/' }
944
+
945
+ const toXprElement = (operand) => {
946
+ if (typeof operand === 'number') return { val: operand }
947
+ if (typeof operand === 'string') return { ref: [operand] }
948
+ return operand // xpr
949
+ }
950
+
951
+ let xpr = [toXprElement(head)]
952
+ for (let i = 0; i < tail.length; i++) {
953
+ xpr.push(opMap[tail[i].op])
954
+ xpr.push(toXprElement(tail[i].operand))
955
+ }
956
+ return tail.length === 0 ? xpr[0] : { xpr }
957
+ }
958
+
959
+ mathOperand
960
+ = integer / negativeIdentifier / identifier / parenthesizedMath
961
+
962
+ negativeIdentifier
963
+ = "-" id:identifier { return { xpr: ['-', { ref: [id] }] } }
964
+
965
+ parenthesizedMath
966
+ = OPEN expr:mathCalc CLOSE { return expr }
967
+
880
968
  comparison
881
969
  = a:operand _ o:$("eq" / "ne" / "lt" / "gt" / "le" / "ge") _ b:operand {
882
970
  return [ a, OPERATORS[o] || o, b ]
@@ -888,9 +976,6 @@
888
976
  = a:operand _ "in" _ b:listFilterParam {
889
977
  return [ a, "in", b ]
890
978
  }
891
-
892
- mathCalc
893
- = operand (_ ("add" / "sub" / "mul" / "div" / "mod") _ operand)*
894
979
 
895
980
  operand
896
981
  = navigationCount / function / val / ref / jsonObject / jsonArray / list
@@ -1025,7 +1110,6 @@
1025
1110
  // REVISIT: All transformations below need improvment
1026
1111
  "search" search:searchTrafo{return search} /
1027
1112
  "concat" con:concatTrafo{return con} / //> Return con so that concat string is not returned
1028
- "compute" compute:computeTrafo{return compute} /
1029
1113
  "top" top:topTrafo{return top} /
1030
1114
  "skip" skip:skipTrafo{return skip} /
1031
1115
  "orderby" order:orderbyTrafo{return order} /
@@ -1054,8 +1138,7 @@
1054
1138
  ) { return res }
1055
1139
  aggregateExpr
1056
1140
  = path:(
1057
- ref
1058
- // / mathCalc - needs CAP support
1141
+ ref
1059
1142
  )
1060
1143
  func:aggregateWith? aggregateFrom? as:asAlias?
1061
1144
  { return { func, args: [ path ], as: as ?? path.ref[0] } }
@@ -1102,11 +1185,6 @@
1102
1185
  return {concat: [trafo1, ...trafo2]}
1103
1186
  }
1104
1187
 
1105
- // REVISIT: support compute - current implementation is deviating from odata
1106
- computeTrafo = OPEN o computeExpr (o COMMA o computeExpr)* o CLOSE
1107
-
1108
- computeExpr = where_clause asAlias
1109
-
1110
1188
  commonFuncTrafo = OPEN o first:operand o COMMA o second:operand o CLOSE { return [first, second] }
1111
1189
 
1112
1190
  // REVISIT: support identity