@sap/cds 9.7.1 → 9.8.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 (44) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/_i18n/i18n_en_US_saptrc.properties +1 -56
  3. package/_i18n/messages_en_US_saptrc.properties +1 -92
  4. package/eslint.config.mjs +4 -1
  5. package/lib/compile/cds-compile.js +1 -0
  6. package/lib/compile/for/direct_crud.js +23 -0
  7. package/lib/compile/for/lean_drafts.js +12 -0
  8. package/lib/compile/for/odata.js +1 -18
  9. package/lib/compile/to/edm.js +1 -0
  10. package/lib/compile/to/json.js +4 -2
  11. package/lib/env/defaults.js +1 -0
  12. package/lib/env/serviceBindings.js +15 -5
  13. package/lib/index.js +1 -1
  14. package/lib/log/cds-error.js +33 -20
  15. package/lib/req/spawn.js +2 -2
  16. package/lib/srv/bindings.js +6 -13
  17. package/lib/srv/cds.Service.js +8 -36
  18. package/lib/srv/protocols/hcql.js +19 -2
  19. package/lib/utils/cds-utils.js +25 -16
  20. package/lib/utils/tar-win.js +106 -0
  21. package/lib/utils/tar.js +23 -158
  22. package/libx/_runtime/common/generic/crud.js +8 -7
  23. package/libx/_runtime/common/generic/sorting.js +7 -3
  24. package/libx/_runtime/common/utils/resolveView.js +47 -40
  25. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -0
  26. package/libx/_runtime/fiori/lean-draft.js +11 -2
  27. package/libx/_runtime/messaging/kafka.js +6 -5
  28. package/libx/_runtime/messaging/service.js +3 -1
  29. package/libx/_runtime/remote/Service.js +3 -0
  30. package/libx/_runtime/remote/utils/client.js +2 -4
  31. package/libx/_runtime/remote/utils/query.js +4 -4
  32. package/libx/odata/middleware/batch.js +323 -339
  33. package/libx/odata/middleware/create.js +0 -5
  34. package/libx/odata/middleware/delete.js +0 -5
  35. package/libx/odata/middleware/operation.js +10 -8
  36. package/libx/odata/middleware/read.js +0 -10
  37. package/libx/odata/middleware/stream.js +1 -0
  38. package/libx/odata/middleware/update.js +0 -6
  39. package/libx/odata/parse/afterburner.js +47 -22
  40. package/libx/odata/parse/cqn2odata.js +6 -1
  41. package/libx/odata/parse/grammar.peggy +14 -2
  42. package/libx/odata/parse/multipartToJson.js +2 -1
  43. package/libx/odata/parse/parser.js +1 -1
  44. package/package.json +2 -2
@@ -92,11 +92,6 @@ module.exports = (adapter, isUpsert) => {
92
92
  .catch(err => {
93
93
  handleSapMessages(cdsReq, req, res)
94
94
 
95
- // REVISIT: invoke service.on('error') for failed batch subrequests
96
- if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
97
- for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
98
- }
99
-
100
95
  next(err)
101
96
  })
102
97
  }
@@ -63,11 +63,6 @@ module.exports = adapter => {
63
63
  .catch(err => {
64
64
  handleSapMessages(cdsReq, req, res)
65
65
 
66
- // REVISIT: invoke service.on('error') for failed batch subrequests
67
- if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
68
- for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
69
- }
70
-
71
66
  next(err)
72
67
  })
73
68
  }
@@ -15,6 +15,7 @@ const {
15
15
  getReadable,
16
16
  validateMimetypeIsAcceptedOrThrow
17
17
  } = require('../../common/utils/streaming')
18
+ const odataBind = require('../utils/odataBind')
18
19
 
19
20
  const _findEdmNameFor = (definition, namespace, fullyQualified = false) => {
20
21
  let name
@@ -62,6 +63,9 @@ module.exports = adapter => {
62
63
 
63
64
  const model = cds.context.model ?? service.model
64
65
 
66
+ // payload & params
67
+ const data = args || req.body
68
+
65
69
  // unbound vs. bound
66
70
  let entity, params
67
71
  if (model.definitions[operation]) {
@@ -77,6 +81,12 @@ module.exports = adapter => {
77
81
  entity = cur
78
82
  const keysAndParams = getKeysAndParamsFromPath(req._query.SELECT.from, { model })
79
83
  params = keysAndParams.params
84
+
85
+ const onCollection = !!(
86
+ operation['@cds.odata.bindingparameter.collection'] ||
87
+ (operation.params && [...operation.params].some(p => p?.items?.type === '$self'))
88
+ )
89
+ if (operation.kind === 'action' && onCollection) odataBind(data, entity)
80
90
  }
81
91
 
82
92
  // validate method
@@ -87,9 +97,6 @@ module.exports = adapter => {
87
97
  return next({ code: 405 })
88
98
  }
89
99
 
90
- // payload & params
91
- const data = args || req.body
92
-
93
100
  // event
94
101
  // REVISIT: when is operation.name actually prefixed with the service name?
95
102
  const event = operation.name.replace(`${service.definition.name}.`, '')
@@ -175,11 +182,6 @@ module.exports = adapter => {
175
182
  .catch(err => {
176
183
  handleSapMessages(cdsReq, req, res)
177
184
 
178
- // REVISIT: invoke service.on('error') for failed batch subrequests
179
- if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
180
- for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
181
- }
182
-
183
185
  next(err)
184
186
  })
185
187
  }
@@ -165,11 +165,6 @@ const _handleArrayOfQueriesFactory = adapter => {
165
165
  .catch(err => {
166
166
  handleSapMessages(cdsReq, req, res)
167
167
 
168
- // REVISIT: invoke service.on('error') for failed batch subrequests
169
- if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
170
- for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
171
- }
172
-
173
168
  next(err)
174
169
  })
175
170
  }
@@ -274,11 +269,6 @@ module.exports = adapter => {
274
269
  .catch(err => {
275
270
  handleSapMessages(cdsReq, req, res)
276
271
 
277
- // REVISIT: invoke service.on('error') for failed batch subrequests
278
- if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
279
- for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
280
- }
281
-
282
272
  next(err)
283
273
  })
284
274
  }
@@ -101,6 +101,7 @@ module.exports = adapter => {
101
101
  break
102
102
  }
103
103
  }
104
+ if (!query._propertyAccess) return next(new cds.error(400))
104
105
  }
105
106
 
106
107
  const pdfMimeType = !!req.headers.accept?.match(/application\/pdf/)
@@ -159,12 +159,6 @@ module.exports = adapter => {
159
159
  return next()
160
160
  }
161
161
 
162
- // REVISIT: invoke service.on('error') for failed batch subrequests
163
- if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
164
- for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
165
- }
166
-
167
- // continue with caught error
168
162
  next(err)
169
163
  })
170
164
  }
@@ -1,4 +1,5 @@
1
1
  const cds = require('../../../lib')
2
+ const DEBUG = cds.debug('odata|rest')
2
3
 
3
4
  const { keysOf, addRefToWhereIfNecessary } = require('../utils')
4
5
 
@@ -20,6 +21,9 @@ function _getDefinition(definition, name, namespace) {
20
21
  definition.actions?.[name] ??
21
22
  definition.actions?.[name.replace(namespace + '.', '')]
22
23
 
24
+ if (DEBUG && def && def['@cds.api.ignore']) {
25
+ DEBUG(`${name} is not served because it is annotated with @cds.api.ignore`)
26
+ }
23
27
  if (def && !def['@cds.api.ignore']) return def
24
28
  }
25
29
 
@@ -452,7 +456,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
452
456
  target = current.returns.items ?? current.returns
453
457
  }
454
458
  } else if (current.isAssociation) {
455
- if (current._target._service !== _get_service_of(target)) {
459
+ if (current._target._service !== target._service) {
456
460
  // not exposed target
457
461
  _doesNotExistError(false, current.name, target.name.replace(namespace + '.', ''), current.kind)
458
462
  }
@@ -569,27 +573,56 @@ function _addKeys(columns, target) {
569
573
  }
570
574
 
571
575
  /**
572
- * Recursively, for each depth, remove all other select columns if a select star is present
573
- * (including duplicates) and remove duplicate expand stars.
576
+ * Recursively, for each depth, remove all other select columns if a select star is present (including duplicates) and
577
+ * remove duplicate expand stars. In case of an expand star, also remove all navigations from the select columns, as
578
+ * they would cause duplicate columns to be selected.
574
579
  *
575
580
  * @param {*} columns CQN `SELECT` columns array.
581
+ * @param {*} target CSN target definition of the current depth.
576
582
  */
577
- function _removeUnneededColumnsIfHasAsterisk(columns) {
583
+ function _removeUnneededColumnsIfHasAsterisk(columns, target) {
584
+ if (!target) return
578
585
  // We need to know if column contains a select * before we can remove other selected columns below
579
586
  const hasSelectStar = columns.some(column => column === '*')
580
587
  let hasExpandStar = false
588
+ let navigationProps
589
+
590
+ const toRemove = []
581
591
 
582
- columns.forEach((column, i) => {
592
+ for (let i = 0; i < columns.length; i++) {
593
+ const column = columns[i]
583
594
  // Remove other select columns if we have a select star
584
- if (hasSelectStar && column.ref && !column.expand) columns.splice(i, 1)
585
- // Remove duplicate expand stars
595
+ if (hasSelectStar && column.ref && !column.expand) toRemove.push(i)
596
+
586
597
  if (!column.ref && column.expand?.[0] === '*') {
587
- if (hasExpandStar) columns.splice(i, 1)
588
- hasExpandStar = true
598
+ if (hasExpandStar) {
599
+ toRemove.push(i) // Remove duplicate expand stars
600
+ } else {
601
+ hasExpandStar = true
602
+ navigationProps = Object.values(target.elements)
603
+ .filter(element => element.isAssociation || element.isComposition)
604
+ .map(element => element.name)
605
+ }
589
606
  }
590
607
  // Recursively remove unneeded columns in expand
591
- if (column.expand) _removeUnneededColumnsIfHasAsterisk(column.expand)
592
- })
608
+ if (column.expand && column.ref)
609
+ _removeUnneededColumnsIfHasAsterisk(column.expand, target.elements?.[column.ref[0]]?._target)
610
+ }
611
+
612
+ if (hasExpandStar && navigationProps?.length) {
613
+ for (let i = 0; i < columns.length; i++) {
614
+ const column = columns[i]
615
+ if (column.ref && !column.expand && column.ref.length === 1 && navigationProps.includes(column.ref[0])) {
616
+ toRemove.push(i)
617
+ }
618
+ }
619
+ }
620
+
621
+ if (!toRemove.length) return
622
+
623
+ // Descending order to avoid issues with splicing while iterating
624
+ const uniqueIndexes = [...new Set(toRemove)].sort((a, b) => b - a)
625
+ for (const index of uniqueIndexes) columns.splice(index, 1)
593
626
  }
594
627
 
595
628
  const _structProperty = (ref, target) => {
@@ -621,7 +654,7 @@ function _processColumns(cqn, target, protocol) {
621
654
  else if (target.kind === 'action' && target.returns?.kind === 'entity') entity = target.returns
622
655
  if (!entity) return
623
656
 
624
- _removeUnneededColumnsIfHasAsterisk(columns)
657
+ _removeUnneededColumnsIfHasAsterisk(columns, target)
625
658
  rewriteExpandAsterisk(columns, entity)
626
659
 
627
660
  // For OData, add missing key fields to columns
@@ -767,12 +800,6 @@ const _doesNotExistError = (isExpand, refName, targetName, targetKind) => {
767
800
  cds.error({ status: 400, message: msg })
768
801
  }
769
802
 
770
- const _get_service_of = element => {
771
- if (element._service) return element._service
772
- if (element.parent) return _get_service_of(element.parent)
773
- return null
774
- }
775
-
776
803
  function _validateXpr(xpr, target, isOne, model, aliases = []) {
777
804
  if (!xpr) return []
778
805
 
@@ -834,10 +861,8 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
834
861
  element = _structProperty(x.ref.slice(1), element)
835
862
  }
836
863
 
837
- const target_service = _get_service_of(target)
838
-
839
- if (!element._target || element._target._service !== target_service) {
840
- const targetName = target_service ? target.name.replace(target_service.name + '.', '') : target.name
864
+ if (!element._target || element._target._service !== target._service) {
865
+ const targetName = target._service ? target.name.replace(target._service.name + '.', '') : target.name
841
866
  _doesNotExistError(true, refName, targetName)
842
867
  }
843
868
 
@@ -12,6 +12,11 @@ const OPERATORS = {
12
12
  '>=': 'ge'
13
13
  }
14
14
 
15
+ const KNOWN_OPERATORS = {
16
+ ...Object.keys(OPERATORS).reduce((acc, cur) => ((acc[cur] = 1), acc), {}),
17
+ ...{ eq: 1, ne: 1, lt: 1, gt: 1, le: 1, ge: 1 }
18
+ }
19
+
15
20
  const LAMBDA_VARIABLE = 'd'
16
21
 
17
22
  const needArrayProps = Object.fromEntries(
@@ -210,7 +215,7 @@ function _xpr(expr, target, kind, isLambda, navPrefix = []) {
210
215
  res.push(OPERATORS[cur] || cur.toLowerCase())
211
216
  }
212
217
  } else {
213
- const ref = expr[i - 1] in OPERATORS ? expr[i - 2] : expr[i + 1] in OPERATORS ? expr[i + 2] : null
218
+ const ref = expr[i - 1] in KNOWN_OPERATORS ? expr[i - 2] : expr[i + 1] in KNOWN_OPERATORS ? expr[i + 2] : null
214
219
  const formatted = _format(
215
220
  cur,
216
221
  ref?.ref && (ref.ref.length ? ref.ref : ref.ref[0]),
@@ -944,7 +944,19 @@
944
944
 
945
945
  const toXprElement = (operand) => {
946
946
  if (typeof operand === 'number') return { val: operand }
947
- if (typeof operand === 'string') return { ref: [operand] }
947
+ if (typeof operand === 'string') {
948
+ const safed = safeNumber(operand)
949
+ if (safed !== operand) {
950
+ return { val: safed }
951
+ } else {
952
+ const parsed = parseFloat(operand)
953
+ if (!isNaN(parsed)) {
954
+ return { val: operand, literal: 'number' }
955
+ } else {
956
+ return { ref: [operand] }
957
+ }
958
+ }
959
+ }
948
960
  return operand // xpr
949
961
  }
950
962
 
@@ -957,7 +969,7 @@
957
969
  }
958
970
 
959
971
  mathOperand
960
- = integer / negativeIdentifier / identifier / parenthesizedMath
972
+ = number / negativeIdentifier / identifier / parenthesizedMath
961
973
 
962
974
  negativeIdentifier
963
975
  = "-" id:identifier { return { xpr: ['-', { ref: [id] }] } }
@@ -69,7 +69,8 @@ const _parseStream = async function* (body, boundary) {
69
69
  body: streamConsumers.json(wrapper).catch(() => {})
70
70
  }
71
71
 
72
- const dependencies = [...req.url.matchAll(/^\/?\$([\d.\-_~a-zA-Z]+)/g)]
72
+ // Ignore /$metadata URLs -> they are not dependencies
73
+ const dependencies = [...req.url.matchAll(/^\/?\$(?!metadata$)([\d.\-_~a-zA-Z]+)/g)]
73
74
  if (dependencies.length) {
74
75
  request.dependsOn = []
75
76
  for (const dependency of dependencies) {