@sap/cds 8.5.0 → 8.6.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 (81) hide show
  1. package/CHANGELOG.md +54 -3
  2. package/_i18n/i18n.properties +4 -7
  3. package/eslint.config.mjs +1 -1
  4. package/lib/compile/etc/properties.js +2 -2
  5. package/lib/compile/for/java.js +15 -3
  6. package/lib/compile/for/lean_drafts.js +44 -34
  7. package/lib/compile/for/nodejs.js +19 -10
  8. package/lib/compile/minify.js +2 -4
  9. package/lib/compile/parse.js +106 -72
  10. package/lib/compile/to/edm.js +19 -9
  11. package/lib/compile/to/hana.js +25 -21
  12. package/lib/compile/to/sql.js +15 -8
  13. package/lib/core/linked-csn.js +10 -4
  14. package/lib/dbs/cds-deploy.js +2 -2
  15. package/lib/env/cds-env.js +76 -66
  16. package/lib/env/defaults.js +1 -0
  17. package/lib/i18n/bundles.js +2 -1
  18. package/lib/i18n/localize.js +2 -2
  19. package/lib/index.js +24 -18
  20. package/lib/ql/CREATE.js +11 -6
  21. package/lib/ql/DELETE.js +12 -9
  22. package/lib/ql/DROP.js +15 -8
  23. package/lib/ql/INSERT.js +19 -14
  24. package/lib/ql/SELECT.js +95 -168
  25. package/lib/ql/UPDATE.js +23 -14
  26. package/lib/ql/UPSERT.js +15 -2
  27. package/lib/ql/Whereable.js +44 -118
  28. package/lib/ql/cds-ql.js +222 -28
  29. package/lib/ql/{Query.js → cds.ql-Query.js} +52 -41
  30. package/lib/ql/cds.ql-predicates.js +133 -0
  31. package/lib/ql/cds.ql-projections.js +111 -0
  32. package/lib/ql/cqn.d.ts +146 -0
  33. package/lib/srv/cds-connect.js +3 -3
  34. package/lib/srv/cds-serve.js +2 -2
  35. package/lib/srv/cds.Service.js +132 -0
  36. package/lib/srv/{srv-api.js → cds.ServiceClient.js} +16 -71
  37. package/lib/srv/cds.ServiceProvider.js +20 -0
  38. package/lib/srv/factory.js +20 -8
  39. package/lib/srv/protocols/hcql.js +2 -3
  40. package/lib/srv/protocols/index.js +3 -3
  41. package/lib/srv/srv-dispatch.js +7 -6
  42. package/lib/srv/srv-handlers.js +103 -113
  43. package/lib/srv/srv-methods.js +14 -14
  44. package/lib/srv/srv-tx.js +5 -3
  45. package/lib/utils/cds-utils.js +2 -2
  46. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +3 -3
  47. package/libx/_runtime/cds.js +2 -1
  48. package/libx/_runtime/common/aspects/service.js +25 -0
  49. package/libx/_runtime/common/generic/auth/index.js +5 -0
  50. package/libx/_runtime/common/generic/auth/restrict.js +36 -14
  51. package/libx/_runtime/common/generic/auth/service.js +24 -0
  52. package/libx/_runtime/common/generic/auth/utils.js +14 -6
  53. package/libx/_runtime/common/generic/etag.js +1 -1
  54. package/libx/_runtime/common/utils/cqn.js +1 -2
  55. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  56. package/libx/_runtime/common/utils/generateOnCond.js +7 -3
  57. package/libx/_runtime/common/utils/postProcess.js +4 -1
  58. package/libx/_runtime/common/utils/restrictions.js +1 -0
  59. package/libx/_runtime/fiori/lean-draft.js +53 -42
  60. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  61. package/libx/_runtime/remote/Service.js +2 -0
  62. package/libx/_runtime/remote/utils/client.js +12 -0
  63. package/libx/odata/ODataAdapter.js +2 -1
  64. package/libx/odata/index.js +5 -3
  65. package/libx/odata/middleware/batch.js +4 -0
  66. package/libx/odata/middleware/create.js +2 -2
  67. package/libx/odata/middleware/delete.js +2 -2
  68. package/libx/odata/middleware/operation.js +2 -2
  69. package/libx/odata/middleware/read.js +14 -12
  70. package/libx/odata/middleware/service-document.js +16 -8
  71. package/libx/odata/middleware/update.js +2 -2
  72. package/libx/odata/parse/afterburner.js +64 -30
  73. package/libx/odata/parse/grammar.peggy +95 -0
  74. package/libx/odata/parse/parser.js +1 -1
  75. package/libx/odata/utils/index.js +6 -1
  76. package/libx/odata/utils/metadata.js +69 -75
  77. package/libx/odata/utils/postProcess.js +24 -3
  78. package/package.json +1 -1
  79. package/server.js +1 -1
  80. package/lib/ql/parse.js +0 -36
  81. /package/lib/ql/{infer.js → cds.ql-infer.js} +0 -0
@@ -19,7 +19,7 @@ function _getDefinition(definition, name, namespace) {
19
19
 
20
20
  function _resolveAliasesInRef(ref, target) {
21
21
  if (ref.length === 1) {
22
- if (target.keys[ref[0]]) return ref
22
+ if (target.keys?.[ref[0]]) return ref
23
23
 
24
24
  // resolve multi-part refs for innermost ref in url
25
25
  if (target._flattenedKeys === undefined) {
@@ -266,6 +266,23 @@ function _handleCollectionBoundActions(current, ref, i, namespace, one) {
266
266
  return incompleteKeys
267
267
  }
268
268
 
269
+ function _resolveImplicitFunctionParameters(args) {
270
+ Object.entries(args).forEach(([key, value]) => {
271
+ if (typeof value !== 'boolean' && !!Number(value)) {
272
+ args[key] = Number(value)
273
+ } else if (typeof value === 'string') {
274
+ let result
275
+ result = value.match(/^'(\w*)'$/)?.[1]
276
+ if (result) {
277
+ args[key] = result
278
+ } else {
279
+ result = value.match(/^binary'([^']+)'$/)?.[1]
280
+ if (result) args[key] = Buffer.from(result, 'base64')
281
+ }
282
+ }
283
+ })
284
+ }
285
+
269
286
  function _processSegments(from, model, namespace, cqn, protocol) {
270
287
  const { ref } = from
271
288
 
@@ -386,16 +403,19 @@ function _processSegments(from, model, namespace, cqn, protocol) {
386
403
 
387
404
  ref[i] = { operation: current.name }
388
405
 
389
- if (params) ref[i].args = _getDataFromParams(params, current)
390
- // REVISIT: SELECT.from._params is a temporary hack
391
- else if (from._params && current.kind === 'function') {
392
- // only take known params to allow additional instructions like sap-language, etc.
393
- ref[i].args = current['@open']
394
- ? Object.assign({}, from._params)
395
- : Object.keys(from._params).reduce((acc, cur) => {
396
- if (current.params && cur in current.params) acc[cur] = from._params[cur]
397
- return acc
398
- }, {})
406
+ if (current.kind === 'function') {
407
+ if (params) ref[i].args = _getDataFromParams(params, current)
408
+ // REVISIT: SELECT.from._params is a temporary hack
409
+ else if (from._params) {
410
+ // only take known params to allow additional instructions like sap-language, etc.
411
+ ref[i].args = current['@open']
412
+ ? Object.assign({}, from._params)
413
+ : Object.keys(from._params).reduce((acc, cur) => {
414
+ if (current.params && cur in current.params) acc[cur] = from._params[cur]
415
+ return acc
416
+ }, {})
417
+ _resolveImplicitFunctionParameters(ref[i].args)
418
+ }
399
419
  }
400
420
  if (current.returns && current.returns._type) one = true
401
421
 
@@ -494,12 +514,11 @@ function _addKeys(columns, target) {
494
514
  let hasAggregatedColumn = false,
495
515
  hasStarColumn = false
496
516
 
497
- for (let k = 0; k < columns.length; k++) {
498
- if (columns[k] === '*') hasStarColumn = true
499
- else if (columns[k].func || columns[k].func === null) hasAggregatedColumn = true
517
+ for (const column of columns) {
518
+ if (column === '*') hasStarColumn = true
519
+ else if (column.func || column.func === null) hasAggregatedColumn = true
500
520
  // Add keys to (sub-)expands
501
- else if (columns[k].expand && columns[k].expand[0] !== '*')
502
- _addKeys(columns[k].expand, target.elements[columns[k].ref]._target)
521
+ else if (column.expand && column.ref) _addKeys(column.expand, target.elements[column.ref]._target)
503
522
  }
504
523
 
505
524
  // Don't add keys to queries with calculated properties, especially aggregations
@@ -515,15 +534,27 @@ function _addKeys(columns, target) {
515
534
  }
516
535
  }
517
536
 
518
- // remove duplicate * in expand (e.g. expand=*,*)
519
- function _removeDuplicateAsterisk(columns) {
537
+ /**
538
+ * Recursively, for each depth, remove all other select columns if a select star is present
539
+ * (including duplicates) and remove duplicate expand stars.
540
+ *
541
+ * @param {*} columns CQN `SELECT` columns array.
542
+ */
543
+ function _removeUnneededColumnsIfHasAsterisk(columns) {
544
+ // We need to know if column contains a select * before we can remove other selected columns below
545
+ const hasSelectStar = columns.some(column => column === '*')
520
546
  let hasExpandStar = false
521
547
 
522
548
  columns.forEach((column, i) => {
549
+ // Remove other select columns if we have a select star
550
+ if (hasSelectStar && column.ref && !column.expand) columns.splice(i, 1)
551
+ // Remove duplicate expand stars
523
552
  if (!column.ref && column.expand?.[0] === '*') {
524
553
  if (hasExpandStar) columns.splice(i, 1)
525
554
  hasExpandStar = true
526
555
  }
556
+ // Recursively remove unneeded columns in expand
557
+ if (column.expand) _removeUnneededColumnsIfHasAsterisk(column.expand)
527
558
  })
528
559
  }
529
560
 
@@ -548,7 +579,7 @@ function _processColumns(cqn, target, protocol) {
548
579
  else if (target.kind === 'action' && target.returns?.kind === 'entity') entity = target.returns
549
580
  if (!entity) return
550
581
 
551
- _removeDuplicateAsterisk(columns)
582
+ _removeUnneededColumnsIfHasAsterisk(columns)
552
583
  rewriteExpandAsterisk(columns, entity)
553
584
  if (protocol !== 'rest') _addKeys(columns, entity)
554
585
  }
@@ -720,24 +751,27 @@ module.exports = (cqn, model, namespace, protocol) => {
720
751
  const from = resolveFromSelect(cqn)
721
752
  const { ref } = from
722
753
 
754
+ let edmName = ref[0].id || ref[0]
723
755
  // REVISIT: shouldn't be necessary
724
- // Second findCsnTargetFor is required for concat query, where the root is already identified with the first query
725
- // and subsequent queries already have correct root
756
+ if (edmName.split('.').length > 1)
757
+ //required for concat query, where the root is already identified with the first query and subsequent queries already have correct root
758
+ edmName = edmName.split('.')[edmName.split('.').length - 1]
759
+
726
760
  /*
727
761
  * make first path segment fully qualified
728
762
  */
729
- const root =
730
- findCsnTargetFor(ref[0].id || ref[0], model, namespace) ||
731
- findCsnTargetFor(
732
- ref[0].id?.split('.')[ref[0].id?.split('.').length - 1] || ref[0].split('.')[ref[0].split('.').length - 1],
733
- model,
734
- namespace
735
- )
736
-
737
- // REVISIT: 404 or 400?
763
+ const root = findCsnTargetFor(edmName, model, namespace)
764
+
738
765
  if (!root) {
766
+ //404 else we would expose knowledge to potential attackers
739
767
  cds.error(`Invalid resource path "${namespace}.${ref[0].id || ref[0]}"`, { code: '404', statusCode: 404 })
740
768
  }
769
+ if (cds.env.effective.odata.containment && model.definitions[namespace]._containedEntities.has(root.name)) {
770
+ cds.error(`Invalid resource path "${namespace}.${ref[0].id || ref[0]}"! It is not an entity set nor a singleton.`, {
771
+ code: '404',
772
+ statusCode: 404
773
+ })
774
+ }
741
775
  if (ref[0].id) ref[0].id = root.name
742
776
  else ref[0] = root.name
743
777
 
@@ -204,6 +204,9 @@
204
204
  ) {
205
205
  cqn.from = { SELECT: { ...cqn } }
206
206
  }
207
+ if (apply.topLevels) cqn.__topLevels = apply.topLevels
208
+ if (apply.ancestors) cqn.__ancestors = apply.ancestors
209
+ if (apply.descendants) cqn.__descendants = apply.descendants
207
210
  if (apply.where) cqn.where = apply.where
208
211
  if (apply.search) cqn.search = apply.search
209
212
  if (apply.groupBy) {
@@ -379,6 +382,19 @@
379
382
  return args
380
383
  }
381
384
 
385
+ // `path` cannot be used in hierarchy functions, hence use a simpler definition
386
+ simplePath
387
+ = i:identifier filter:(OPEN CLOSE/OPEN a:args CLOSE{ return a })? tail:("/" s:simplePath{ return s })*{
388
+ const ref = [filter ? { id: i, where: filter } : i]
389
+ if (tail.length) ref.push(...tail.map(t => t.from.ref[0]))
390
+ return {
391
+ from: {
392
+ ref
393
+ }
394
+ }
395
+ }
396
+
397
+
382
398
  //
383
399
  // ---------- Query Options ------------
384
400
 
@@ -778,6 +794,9 @@
778
794
  "top" top:topTrafo{return top} /
779
795
  "skip" skip:skipTrafo{return skip} /
780
796
  "orderby" order:orderbyTrafo{return order} /
797
+ "com.sap.vocabularies.Hierarchy.v1.TopLevels" toplevels:topLevelsTrafo{return toplevels} /
798
+ "ancestors" ancestors:ansDescTrafo{return { ancestors: ancestors }} /
799
+ "descendants" descendants:ansDescTrafo{return { descendants: descendants }} /
781
800
  func:("topcount"i/"bottomcount"i/"topsum"i/"bottomsum"i/"toppercent"i/"bottompercent"i) args:commonFuncTrafo {
782
801
  func = func.toLowerCase()
783
802
  if (!SUPPORTED_APPLY_TRANSFORMATIONS[func]) {
@@ -873,6 +892,82 @@
873
892
  return {orderBy: [o,...o2]}
874
893
  }
875
894
 
895
+ topLevelsTrafo
896
+ = OPEN o t:topLevelsArg ts:(o COMMA o t2:topLevelsArg{ return t2 })* o CLOSE {
897
+ ts.forEach(_t => Object.assign(t, _t))
898
+ return {
899
+ topLevels: t
900
+ }
901
+ }
902
+
903
+ topLevelsArg
904
+ = hierarchyNodes /
905
+ hierarchyQualifier /
906
+ nodeProperty /
907
+ levels /
908
+ expandLevels
909
+
910
+ hierarchyNodes
911
+ = o "HierarchyNodes" o "=" o "$root/" p:simplePath o {
912
+ return { hierarchyNodes: p }
913
+ }
914
+
915
+ hierarchyQualifier
916
+ = o "HierarchyQualifier" o "=" o s:string o {
917
+ return { hierarchyQualifier: s }
918
+ }
919
+
920
+ nodeProperty
921
+ = o "NodeProperty" o "=" o s:string o {
922
+ return { nodeProperty: s }
923
+ }
924
+
925
+ levels
926
+ = o "Levels" o "=" o i:integer o {
927
+ return { levels: i}
928
+ }
929
+
930
+ expandLevels
931
+ = o "ExpandLevels" o "=" o "[" o e:expandItem es:(o COMMA o e2:expandItem{ return e2 })* "]" {
932
+ return { expandLevels: [e, ...es] }
933
+ }
934
+
935
+ expandItem
936
+ = o "{" o e1:expandItemProp o "," o e2:expandItemProp o "}" {
937
+ return Object.assign(e1,e2)
938
+ }
939
+
940
+ expandItemProp
941
+ = "\"NodeID\"" o ":" o s:doubleQuotedString{return { nodeID: s } } /
942
+ "\"Levels\"" o ":" o i:integer{return { levels: i } }
943
+
944
+ ansDescTrafo
945
+ = OPEN o "$root/" p:simplePath o COMMA o h:identifier o COMMA o e:identifier o COMMA? t:ansDescGetNodes? o COMMA? d:integer? o COMMA? k:ansDescKeepStart? CLOSE {
946
+ return {
947
+ path: p,
948
+ hierarchy: h,
949
+ id: e,
950
+ nodes: t,
951
+ keepStart: k?.keepStart || false
952
+ }
953
+ }
954
+
955
+ ansDescGetNodes
956
+ = g:ansDescGetNode gs:( o "/" o g2:ansDescGetNode{return g2} )*{ return [g, ...gs] }
957
+
958
+ ansDescGetNode
959
+ = ansDescSearchNode /
960
+ ansDescFilterNode
961
+
962
+ ansDescFilterNode
963
+ = "filter(" f:filter ")"{ return { filter: f } }
964
+
965
+ ansDescSearchNode
966
+ = "search(" s:search_expand ")"{ return { search: s } }
967
+
968
+ ansDescKeepStart
969
+ = "keep start"{ return { keepStart: true } }
970
+
876
971
  //
877
972
  // ---------- Literals -----------
878
973