@sap/cds 9.2.0 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/_i18n/i18n_es.properties +3 -3
  3. package/_i18n/i18n_es_MX.properties +3 -3
  4. package/_i18n/i18n_fr.properties +2 -2
  5. package/_i18n/messages.properties +6 -0
  6. package/app/index.js +0 -1
  7. package/bin/deploy.js +1 -1
  8. package/bin/serve.js +7 -20
  9. package/lib/compile/cdsc.js +3 -0
  10. package/lib/compile/for/flows.js +102 -0
  11. package/lib/compile/for/nodejs.js +28 -0
  12. package/lib/compile/to/edm.js +11 -4
  13. package/lib/core/classes.js +1 -1
  14. package/lib/core/linked-csn.js +8 -0
  15. package/lib/dbs/cds-deploy.js +12 -12
  16. package/lib/env/cds-env.js +1 -1
  17. package/lib/env/cds-requires.js +21 -20
  18. package/lib/env/defaults.js +2 -1
  19. package/lib/index.js +5 -6
  20. package/lib/log/cds-log.js +6 -5
  21. package/lib/log/format/aspects/cf.js +2 -2
  22. package/lib/plugins.js +1 -1
  23. package/lib/ql/UPDATE.js +3 -1
  24. package/lib/ql/cds-ql.js +0 -3
  25. package/lib/req/request.js +3 -3
  26. package/lib/req/response.js +12 -7
  27. package/lib/srv/bindings.js +17 -17
  28. package/lib/srv/cds-connect.js +6 -9
  29. package/lib/srv/cds-serve.js +74 -137
  30. package/lib/srv/cds.Service.js +49 -0
  31. package/lib/srv/factory.js +4 -4
  32. package/lib/srv/middlewares/auth/ias-auth.js +31 -11
  33. package/lib/srv/middlewares/auth/index.js +3 -2
  34. package/lib/srv/middlewares/auth/jwt-auth.js +19 -6
  35. package/lib/srv/protocols/hcql.js +16 -1
  36. package/lib/srv/srv-dispatch.js +1 -1
  37. package/lib/utils/cds-utils.js +4 -8
  38. package/lib/utils/csv-reader.js +27 -7
  39. package/libx/_runtime/cds.js +0 -6
  40. package/libx/_runtime/common/Service.js +5 -0
  41. package/libx/_runtime/common/generic/crud.js +1 -1
  42. package/libx/_runtime/common/generic/flows.js +106 -0
  43. package/libx/_runtime/common/generic/paging.js +3 -3
  44. package/libx/_runtime/common/utils/differ.js +5 -15
  45. package/libx/_runtime/common/utils/resolveView.js +2 -2
  46. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -2
  47. package/libx/_runtime/fiori/lean-draft.js +76 -40
  48. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  49. package/libx/_runtime/messaging/file-based.js +2 -1
  50. package/libx/_runtime/remote/Service.js +68 -62
  51. package/libx/_runtime/remote/utils/client.js +29 -216
  52. package/libx/_runtime/remote/utils/query.js +197 -0
  53. package/libx/_runtime/ucl/Service.js +180 -112
  54. package/libx/_runtime/ucl/queries.js +61 -0
  55. package/libx/odata/ODataAdapter.js +1 -4
  56. package/libx/odata/index.js +2 -10
  57. package/libx/odata/middleware/error.js +8 -1
  58. package/libx/odata/middleware/stream.js +1 -1
  59. package/libx/odata/middleware/update.js +12 -2
  60. package/libx/odata/parse/afterburner.js +113 -20
  61. package/libx/odata/parse/cqn2odata.js +1 -3
  62. package/libx/odata/parse/grammar.peggy +4 -2
  63. package/libx/odata/parse/parser.js +1 -1
  64. package/libx/queue/index.js +1 -1
  65. package/libx/rest/middleware/parse.js +9 -2
  66. package/package.json +2 -2
  67. package/server.js +2 -0
  68. package/srv/app-service.js +1 -0
  69. package/srv/db-service.js +1 -0
  70. package/srv/msg-service.js +1 -0
  71. package/srv/remote-service.js +1 -0
  72. package/srv/ucl-service.cds +32 -0
  73. package/srv/ucl-service.js +1 -0
  74. package/lib/ql/resolve.js +0 -45
  75. package/libx/common/assert/type-strict.js +0 -109
  76. package/libx/common/assert/utils.js +0 -60
@@ -8,6 +8,9 @@ const normalizeTimestamp = require('../../_runtime/common/utils/normalizeTimesta
8
8
  const { rewriteExpandAsterisk } = require('../../_runtime/common/utils/rewriteAsterisks')
9
9
  const resolveStructured = require('../../_runtime/common/utils/resolveStructured')
10
10
 
11
+ // Same regex as peggy parser
12
+ const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{12}$/i
13
+
11
14
  function _getDefinition(definition, name, namespace) {
12
15
  return (
13
16
  definition?.definitions?.[name] ||
@@ -185,6 +188,13 @@ function _convertVal(value, element) {
185
188
  case 'cds.Timestamp':
186
189
  return normalizeTimestamp(value)
187
190
 
191
+ case 'cds.UUID':
192
+ if (!RELAXED_UUID_REGEX.test(value)) {
193
+ const msg = `Element "${element.name}" does not contain a valid UUID`
194
+ throw Object.assign(new Error(msg), { statusCode: 400 })
195
+ }
196
+ return value
197
+
188
198
  default:
189
199
  return value
190
200
  }
@@ -528,6 +538,10 @@ function _processSegments(from, model, namespace, cqn, protocol) {
528
538
 
529
539
  const AGGR_DFLT = '@Aggregation.default'
530
540
  const CSTM_AGGR = '@Aggregation.CustomAggregate'
541
+ const SMTCS_CC = '@Semantics.currencyCode'
542
+ const SMTCS_UOM = '@Semantics.unitOfMeasure'
543
+ const SMTCS_AMT_CC = '@Semantics.amount.currencyCode'
544
+ const SMTCS_AMT_UOM = '@Semantics.amount.unitOfMeasure'
531
545
 
532
546
  function _addKeys(columns, target) {
533
547
  let hasAggregatedColumn = false,
@@ -586,17 +600,20 @@ const _structProperty = (ref, target) => {
586
600
  }
587
601
 
588
602
  function _processColumns(cqn, target, protocol) {
603
+ // Recursively process columns for nested SELECTs
589
604
  if (cqn.SELECT.from.SELECT) _processColumns(cqn.SELECT.from, target)
590
605
 
591
606
  let columns = cqn.SELECT.columns
592
607
 
608
+ // Error if groupBy is present but no columns are selected
593
609
  if (columns && !columns.length && cqn.SELECT.groupBy) {
594
- cds.error('Explicit select must include at least one column available in the result set of groupby!', {
610
+ throw cds.error('Explicit select must include at least one column available in the result set of groupby', {
595
611
  code: '400',
596
612
  statusCode: 400
597
613
  })
598
614
  }
599
615
 
616
+ // If columns exist and no groupBy, handle asterisk and expand logic
600
617
  if (columns && !cqn.SELECT.groupBy) {
601
618
  let entity
602
619
  if (target.kind === 'entity') entity = target
@@ -606,32 +623,108 @@ function _processColumns(cqn, target, protocol) {
606
623
  _removeUnneededColumnsIfHasAsterisk(columns)
607
624
  rewriteExpandAsterisk(columns, entity)
608
625
 
609
- // in case of odata, add all missing key fields (i.e., not in $select)
626
+ // For OData, add missing key fields to columns
610
627
  if (protocol?.match(/odata/i)) _addKeys(columns, entity)
611
628
  }
612
629
 
613
630
  if (!Array.isArray(columns)) return
614
631
 
615
- let aggrProp, aggrElem, defaultAggregation
632
+ // Iterate columns to set default aggregation function if needed
616
633
  for (let i = 0; i < columns.length; i++) {
617
- if (
618
- columns[i].func === null &&
619
- columns[i].args &&
620
- columns[i].args.length &&
621
- columns[i].args[0].ref &&
622
- columns[i].args[0].ref.length
623
- ) {
624
- // REVISIT: also support aggregate(Sales/Amount)?
625
- aggrProp = columns[i].args[0].ref[0]
626
- aggrElem = target.elements[aggrProp]
627
- if (aggrElem && target[`${CSTM_AGGR}#${aggrProp}`] && aggrElem[AGGR_DFLT] && aggrElem[AGGR_DFLT]['#']) {
628
- defaultAggregation = aggrElem[AGGR_DFLT]['#'].toLowerCase()
629
- if (defaultAggregation === 'count_distinct') defaultAggregation = 'countdistinct'
630
- columns[i].func = defaultAggregation
631
- columns[i].as = columns[i].as || aggrProp
632
- } else {
633
- throw new Error(`Default aggregation for property "${aggrProp}" not found`)
634
+ const processedColumn = columns[i]
635
+
636
+ // Skip if column is not an object or has a ref (not an aggregation)
637
+ if (typeof processedColumn !== 'object' || processedColumn.ref) continue
638
+
639
+ if (!processedColumn.args?.length) continue
640
+ if (!processedColumn.args[0].ref?.length) continue
641
+ const processedColumnRef = processedColumn.args[0].ref
642
+
643
+ // Extract relevant element characteristics
644
+ let aggregatedPropertyName, aggregatedElement
645
+ for (let refIdx = 0; refIdx < processedColumnRef.length; refIdx++) {
646
+ aggregatedPropertyName = processedColumnRef[refIdx]
647
+ if (aggregatedPropertyName.id) aggregatedPropertyName = aggregatedPropertyName.id
648
+ if (aggregatedElement && aggregatedElement.isAssociation) target = aggregatedElement._target
649
+ aggregatedElement = target.elements[aggregatedPropertyName]
650
+ }
651
+ const prefixRef = processedColumnRef.slice(0, processedColumnRef.length - 1)
652
+ const aggregatedElementRef = [...prefixRef, aggregatedPropertyName]
653
+ const isCurrencyCodeOrUnitOfMeasure = !!(aggregatedElement[SMTCS_CC] || aggregatedElement[SMTCS_UOM])
654
+ processedColumn.as = processedColumn.as || aggregatedPropertyName
655
+
656
+ // Specifically handle aggregating semantic amounts
657
+ if (isCurrencyCodeOrUnitOfMeasure) {
658
+ columns[i] = {
659
+ xpr: [
660
+ 'case',
661
+ 'when',
662
+ { xpr: [{ func: 'max', args: [{ ref: aggregatedElementRef }] }, '=', { val: null }] },
663
+ 'then',
664
+ { val: '' },
665
+ 'else',
666
+ {
667
+ xpr: [
668
+ 'case',
669
+ 'when',
670
+ {
671
+ xpr: [
672
+ { func: 'min', args: [{ ref: aggregatedElementRef }] },
673
+ '=',
674
+ { func: 'max', args: [{ ref: aggregatedElementRef }] }
675
+ ]
676
+ },
677
+ 'then',
678
+ { func: 'min', args: [{ ref: aggregatedElementRef }] },
679
+ 'else',
680
+ { val: null },
681
+ 'end'
682
+ ]
683
+ },
684
+ 'end'
685
+ ],
686
+ as: processedColumn.as
634
687
  }
688
+
689
+ continue
690
+ }
691
+
692
+ // Determine default aggregation function if necessary
693
+ const customAggregate = target[`${CSTM_AGGR}#${aggregatedPropertyName}`]
694
+ if (processedColumn.func === null) {
695
+ processedColumn.func = aggregatedElement?.[AGGR_DFLT]?.['#']?.toLowerCase()
696
+ if (!customAggregate)
697
+ throw cds.error(`Result type for custom aggregation of property "${aggregatedPropertyName}" not found`)
698
+ if (!processedColumn.func)
699
+ throw cds.error(`Default aggregation for property "${aggregatedPropertyName}" not found`)
700
+ if (processedColumn.func === 'count_distinct') processedColumn.func = 'countdistinct'
701
+ }
702
+
703
+ // Process Semantics Amount - Currency Code / Unit of Measure - if present
704
+ const semanticsAmountElementName = aggregatedElement?.[SMTCS_AMT_CC] ?? aggregatedElement?.[SMTCS_AMT_UOM]
705
+ if (!semanticsAmountElementName) continue
706
+ if (!target.elements[semanticsAmountElementName])
707
+ throw cds.error(`Referenced semantics amount element not found: ${semanticsAmountElementName}`)
708
+ const semanticsAmountElementRef = [...prefixRef, semanticsAmountElementName]
709
+
710
+ columns[i] = {
711
+ xpr: [
712
+ 'case',
713
+ 'when',
714
+ {
715
+ xpr: [
716
+ { func: 'min', args: [{ ref: semanticsAmountElementRef }] },
717
+ '=',
718
+ { func: 'max', args: [{ ref: semanticsAmountElementRef }] }
719
+ ]
720
+ },
721
+ 'then',
722
+ { func: processedColumn.func, args: [{ ref: aggregatedElementRef }] },
723
+ 'else',
724
+ { val: null },
725
+ 'end'
726
+ ],
727
+ as: processedColumn.as
635
728
  }
636
729
  }
637
730
  }
@@ -598,7 +598,7 @@ const _delete = (cqn, kind, model) => {
598
598
  return { method: 'DELETE', path: `${url}${keys}` }
599
599
  }
600
600
 
601
- function cqn2odata(cqn, { kind, model, method }) {
601
+ module.exports.cqn2odata = (cqn, { kind, model, method }) => {
602
602
  if (cqn.SELECT) return _select(cqn, kind, model)
603
603
  if (cqn.INSERT) return _insert(cqn, kind, model)
604
604
  if (cqn.UPDATE) return _update(cqn, kind, model, method)
@@ -606,5 +606,3 @@ function cqn2odata(cqn, { kind, model, method }) {
606
606
 
607
607
  throw new Error('Unknown CQN object cannot be translated to URL: ' + JSON.stringify(cqn))
608
608
  }
609
-
610
- module.exports = cqn2odata
@@ -208,7 +208,7 @@
208
208
  if (
209
209
  (topCqn.columns && topCqn.columns[0].as === '$count') ||
210
210
  //In QUERY_WITH_AGGREGATION topCqn.where is a having and thus no further nesting needed
211
- (!QUERY_WITH_AGGREGATION && topCqn.where && (cqn.SELECT.where || cqn.SELECT.limit)) ||
211
+ (!QUERY_WITH_AGGREGATION && topCqn.where && (cqn.SELECT.where || cqn.SELECT.limit)) ||
212
212
  (cqn.SELECT.limit && topCqn.limit) ||
213
213
  (cqn.SELECT.orderBy && topCqn.orderBy) ||
214
214
  (cqn.SELECT.search && topCqn.search)
@@ -712,6 +712,8 @@
712
712
  = (
713
713
  c:('*' / ref) {
714
714
  const col = c === '*' ? {} : c
715
+ if (col.ref?.length > 1) throw Object.assign(new Error(`navigation "${col.ref.join('/')}" in "$expand" is not supported`), { statusCode: 400 })
716
+
715
717
  col.expand = ['*']
716
718
  if (!Array.isArray(SELECT.expand)) SELECT.expand = []
717
719
  if (!SELECT.expand.find(_compareRefs(col))) SELECT.expand.push(col)
@@ -986,7 +988,7 @@
986
988
  // Loop through each element, add it to current level, if element is already part of result, increase level
987
989
  for(let trafos of additionalTransformation) {
988
990
  for(const transformation in trafos) {
989
- if (transformation === 'where' && (mainTransformation.groupBy || mainTransformation.aggregate) && !mainTransformation.having) {
991
+ if (transformation === 'where' && (mainTransformation.groupBy || mainTransformation.aggregate) && !mainTransformation.having) {
990
992
  //When a group by or aggregate preceed a where, the where is a having
991
993
  mainTransformation.having = trafos[transformation]
992
994
  }