@sap/cds 8.2.3 → 8.3.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 (48) hide show
  1. package/CHANGELOG.md +44 -3
  2. package/bin/test.js +1 -1
  3. package/lib/compile/etc/_localized.js +1 -0
  4. package/lib/compile/etc/csv.js +1 -1
  5. package/lib/compile/for/lean_drafts.js +5 -0
  6. package/lib/dbs/cds-deploy.js +8 -5
  7. package/lib/env/cds-requires.js +0 -13
  8. package/lib/linked/validate.js +11 -9
  9. package/lib/log/cds-error.js +10 -7
  10. package/lib/plugins.js +8 -3
  11. package/lib/srv/middlewares/cds-context.js +1 -1
  12. package/lib/srv/middlewares/errors.js +5 -3
  13. package/lib/srv/protocols/index.js +4 -4
  14. package/lib/srv/srv-methods.js +1 -0
  15. package/lib/utils/cds-test.js +2 -1
  16. package/lib/utils/cds-utils.js +14 -1
  17. package/lib/utils/colors.js +45 -44
  18. package/libx/_runtime/common/composition/data.js +4 -2
  19. package/libx/_runtime/common/composition/index.js +1 -2
  20. package/libx/_runtime/common/composition/tree.js +1 -24
  21. package/libx/_runtime/common/error/frontend.js +18 -4
  22. package/libx/_runtime/common/generic/auth/restrict.js +29 -4
  23. package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
  24. package/libx/_runtime/common/i18n/messages.properties +1 -1
  25. package/libx/_runtime/common/utils/cqn.js +0 -26
  26. package/libx/_runtime/common/utils/csn.js +0 -14
  27. package/libx/_runtime/common/utils/differ.js +1 -0
  28. package/libx/_runtime/common/utils/resolveView.js +28 -9
  29. package/libx/_runtime/common/utils/templateProcessor.js +3 -0
  30. package/libx/_runtime/fiori/lean-draft.js +30 -12
  31. package/libx/_runtime/types/api.js +1 -1
  32. package/libx/_runtime/ucl/Service.js +2 -2
  33. package/libx/common/utils/path.js +1 -4
  34. package/libx/odata/ODataAdapter.js +6 -0
  35. package/libx/odata/middleware/batch.js +7 -9
  36. package/libx/odata/middleware/create.js +4 -2
  37. package/libx/odata/middleware/delete.js +3 -1
  38. package/libx/odata/middleware/operation.js +7 -5
  39. package/libx/odata/middleware/read.js +14 -10
  40. package/libx/odata/middleware/service-document.js +1 -1
  41. package/libx/odata/middleware/stream.js +1 -0
  42. package/libx/odata/middleware/update.js +5 -3
  43. package/libx/odata/parse/afterburner.js +37 -49
  44. package/libx/odata/utils/index.js +3 -2
  45. package/libx/odata/utils/postProcess.js +3 -8
  46. package/package.json +1 -1
  47. package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
  48. package/libx/_runtime/messaging/event-broker.js +0 -317
@@ -12,19 +12,19 @@ const { getKeysAndParamsFromPath } = require('../../common/utils')
12
12
  const { getPageSize } = require('../../_runtime/common/generic/paging')
13
13
  const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
14
14
 
15
- const _getCount = result =>
16
- Array.isArray(result)
17
- ? result.reduce((acc, val) => {
18
- return acc + ((val && (val.$count || val._counted_)) || (val[0] && (val[0].$count || val[0]._counted_))) || 0
19
- }, 0)
20
- : result.$count || result._counted_ || 0
15
+ const _getCount = result => (Array.isArray(result) && result.length ? result[0].$count || 0 : result.$count || 0)
21
16
 
22
17
  const _setNextLink = (req, result) => {
23
18
  const $skiptoken = result.$nextLink ?? _calculateSkiptoken(req, result)
24
19
  if (!$skiptoken) return
25
20
 
26
21
  const queryParamsWithSkipToken = { ...req.req.query, $skiptoken }
27
- result.$nextLink = req.req.path.slice(1) + '?' + querystring.stringify(queryParamsWithSkipToken)
22
+ const encodedQueryParams = querystring.stringify(queryParamsWithSkipToken)
23
+
24
+ // percent-encode all path segments with key values inside parentheses, but keep Navigation Properties untouched
25
+ const encodedPath = req.req.path.slice(1).replace(/\('([^']*)'\)/g, (match, key) => `('${encodeURIComponent(key)}')`)
26
+
27
+ result.$nextLink = `${encodedPath}?${encodedQueryParams}`
28
28
  }
29
29
 
30
30
  const _calculateSkiptoken = (req, result) => {
@@ -184,6 +184,8 @@ module.exports = adapter => {
184
184
  // $apply with concat -> multiple queries with special handling
185
185
  if (Array.isArray(req._query)) return _handleArrayOfQueries(req, res, next)
186
186
 
187
+ const model = cds.context.model ?? service.model
188
+
187
189
  // REVISIT: better solution for _propertyAccess
188
190
  let {
189
191
  SELECT: { from, one },
@@ -193,7 +195,7 @@ module.exports = adapter => {
193
195
  const { _query: query } = req
194
196
 
195
197
  // payload & params
196
- const { keys, params } = getKeysAndParamsFromPath(from, service)
198
+ const { keys, params } = getKeysAndParamsFromPath(from, { model })
197
199
  const data = keys //> for read and delete, we provide keys in req.data
198
200
 
199
201
  // cdsReq.headers should contain merged headers of envelope and subreq
@@ -212,7 +214,7 @@ module.exports = adapter => {
212
214
 
213
215
  if (!query.SELECT.columns) query.SELECT.columns = ['*']
214
216
 
215
- handleStreamProperties(target, query.SELECT.columns, service.model)
217
+ handleStreamProperties(target, query.SELECT.columns, model)
216
218
 
217
219
  // REVISIT: what is this for? some tests fail without it... we should find a better solution!
218
220
  Object.defineProperty(query.SELECT, '_4odata', { value: true })
@@ -246,10 +248,12 @@ module.exports = adapter => {
246
248
  if (result == null) {
247
249
  result = []
248
250
  if (req.query.$count) result.$count = 0
251
+ } else if (query.SELECT.count && !result.$count) {
252
+ result.$count = 0
249
253
  }
250
254
 
251
255
  if (!one) _setNextLink(cdsReq, result)
252
- postProcess(cdsReq.target, service, result)
256
+ postProcess(cdsReq.target, model, result)
253
257
  if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
254
258
 
255
259
  const lastSeg = req.path.split('/').slice(-1)[0]
@@ -22,7 +22,7 @@ module.exports = adapter => {
22
22
  throw Object.assign(new Error(msg), { statusCode: 405 })
23
23
  }
24
24
 
25
- const model = cds.context.model || cds.model
25
+ const model = cds.context.model ?? service.model
26
26
  const csnService = model.definitions[service.definition.name]
27
27
 
28
28
  if (req.headers['if-match']) {
@@ -170,6 +170,7 @@ module.exports = adapter => {
170
170
 
171
171
  if (isRedirect(query)) {
172
172
  const cdsReq = adapter.request4({ query, req, res })
173
+
173
174
  service.dispatch(cdsReq).then(result => {
174
175
  if (result[query._propertyAccess]) res.set('Location', result[query._propertyAccess])
175
176
  return res.sendStatus(307)
@@ -69,9 +69,11 @@ module.exports = adapter => {
69
69
  throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
70
70
  }
71
71
 
72
+ const model = cds.context.model ?? service.model
73
+
72
74
  // payload & params
73
75
  const data = _propertyAccess ? { [_propertyAccess]: req.body.value } : req.body
74
- const { keys, params } = getKeysAndParamsFromPath(from, service)
76
+ const { keys, params } = getKeysAndParamsFromPath(from, { model })
75
77
  // add keys from url into payload (overwriting if already present)
76
78
  if (!_propertyAccess) Object.assign(data, keys)
77
79
 
@@ -111,7 +113,7 @@ module.exports = adapter => {
111
113
  if (result == null) return res.sendStatus(204)
112
114
 
113
115
  const isMinimal = getPreferReturnHeader(req) === 'minimal'
114
- postProcess(cdsReq.target, service, result, isMinimal)
116
+ postProcess(cdsReq.target, model, result, isMinimal)
115
117
 
116
118
  if (isMinimal && !target._isSingleton) {
117
119
  // determine calculation based on result with req.data as fallback
@@ -139,7 +141,7 @@ module.exports = adapter => {
139
141
  // PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
140
142
  if (req.headers['if-match']) return next(Object.assign(new Error('412'), { statusCode: 412 }))
141
143
 
142
- if (!upsertSupported(from, service.model)) return next(Object.assign(new Error('422'), { statusCode: 422 }))
144
+ if (!upsertSupported(from, model)) return next(Object.assign(new Error('422'), { statusCode: 422 }))
143
145
 
144
146
  // -> forward to POST
145
147
  req.method = 'POST'
@@ -383,6 +383,14 @@ function _processSegments(from, model, namespace, cqn, protocol) {
383
383
  ref[i] = { operation: current.name }
384
384
  if (params) ref[i].args = _getDataFromParams(params, current)
385
385
  if (current.returns && current.returns._type) one = true
386
+
387
+ if (current.returns) {
388
+ if (current.returns._type) {
389
+ one = true
390
+ }
391
+
392
+ target = current.returns.items ?? current.returns
393
+ }
386
394
  } else if (current.isAssociation) {
387
395
  if (!current._target._service) {
388
396
  // not exposed target
@@ -589,23 +597,26 @@ const _checkAllKeysProvided = (params, entity) => {
589
597
  }
590
598
  }
591
599
 
592
- const _doesNotExistError = (isExpand, refName, targetName) => {
600
+ const _doesNotExistError = (isExpand, refName, targetName, targetKind) => {
593
601
  const msg = isExpand
594
602
  ? `Navigation property "${refName}" is not defined in "${targetName}"`
595
- : `Property "${refName}" does not exist in "${targetName}"`
603
+ : `Property "${refName}" does not exist in ${targetKind === 'type' ? 'type ' : ''}"${targetName}"`
596
604
  throw Object.assign(new Error(msg), { statusCode: 400 })
597
605
  }
598
606
 
599
- function _validateXpr(xpr, ignoredColumns, target, isOne, model, aliases = []) {
607
+ function _validateXpr(xpr, target, isOne, model, aliases = []) {
600
608
  if (!xpr) return []
601
609
 
610
+ const ignoredColumns = Object.values(target.elements ?? {})
611
+ .filter(element => element['@cds.api.ignore'] && !element.isAssociation)
612
+ .map(element => element.name)
602
613
  const _aliases = []
603
614
 
604
615
  for (const x of xpr) {
605
616
  if (x.as) _aliases.push(x.as)
606
617
 
607
618
  if (x.xpr) {
608
- _validateXpr(x.xpr, ignoredColumns, target, isOne, model)
619
+ _validateXpr(x.xpr, target, isOne, model)
609
620
  continue
610
621
  }
611
622
 
@@ -614,101 +625,82 @@ function _validateXpr(xpr, ignoredColumns, target, isOne, model, aliases = []) {
614
625
 
615
626
  if (x.ref[0].where) {
616
627
  const element = target.elements[refName]
617
-
618
628
  if (!element) {
619
629
  _doesNotExistError(true, refName, target.name)
620
630
  }
621
- _validateXpr(x.ref[0].where, ignoredColumns, element._target ?? element.items, isOne, model)
631
+ _validateXpr(x.ref[0].where, element._target ?? element.items, isOne, model)
622
632
  }
623
633
 
624
634
  if (!target?.elements) {
625
- _doesNotExistError(false, refName, target.name)
635
+ _doesNotExistError(false, refName, target.name, target.kind)
626
636
  }
627
637
 
628
638
  if (ignoredColumns.includes(refName) || (!target.elements[refName] && !aliases.includes(refName))) {
629
639
  _doesNotExistError(x.expand, refName, target.name)
630
640
  } else if (x.ref.length > 1) {
631
641
  const element = target.elements[refName]
632
-
633
642
  if (element.isAssociation) {
634
643
  // navigation
635
- const _target = element._target
636
- const _ignoredColumns = Object.values(_target.elements ?? {})
637
- .filter(element => element['@cds.api.ignore'])
638
- .map(element => element.name)
639
- if (element.is2one) {
640
- _validateXpr([{ ref: x.ref.slice(1) }], _ignoredColumns, _target, false, model)
641
- } else {
642
- _validateXpr([{ ref: x.ref.slice(1) }], _ignoredColumns, _target, false, model)
643
- }
644
+ _validateXpr([{ ref: x.ref.slice(1) }], element._target, false, model)
644
645
  } else if (element.kind === 'element') {
645
646
  // structured
646
- _validateXpr([{ ref: x.ref.slice(1) }], ignoredColumns, element, isOne, model)
647
+ _validateXpr([{ ref: x.ref.slice(1) }], element, isOne, model)
647
648
  } else {
648
649
  throw new Error('not yet validated')
649
650
  }
650
651
  }
652
+
651
653
  if (x.expand) {
652
654
  let element = target.elements[refName]
653
655
  if (element.kind === 'element' && element.elements) {
654
656
  // structured
655
- _validateXpr([{ ref: x.ref.slice(1) }], ignoredColumns, element, isOne, model)
657
+ _validateXpr([{ ref: x.ref.slice(1) }], element, isOne, model)
656
658
  element = _structProperty(x.ref.slice(1), element)
657
659
  }
658
-
659
660
  if (!element._target) {
660
661
  _doesNotExistError(true, refName, target.name)
661
662
  }
662
-
663
- const _ignoredColumns = Object.values(element._target.elements ?? {})
664
- .filter(element => element['@cds.api.ignore'])
665
- .map(element => element.name)
666
- _validateXpr(x.expand, _ignoredColumns, element._target, false, model)
667
-
663
+ _validateXpr(x.expand, element._target, false, model)
668
664
  if (x.where) {
669
- _validateXpr(x.where, _ignoredColumns, element._target, false, model)
665
+ _validateXpr(x.where, element._target, false, model)
670
666
  }
671
-
672
667
  if (x.orderBy) {
673
- _validateXpr(x.orderBy, _ignoredColumns, element._target, false, model)
668
+ _validateXpr(x.orderBy, element._target, false, model)
674
669
  }
675
670
  }
676
671
  }
677
672
 
678
673
  if (x.func) {
679
- _validateXpr(x.args, ignoredColumns, target, isOne, model)
674
+ _validateXpr(x.args, target, isOne, model)
680
675
  continue
681
676
  }
682
677
 
683
678
  if (x.SELECT) {
684
679
  const { target } = targetFromPath(x.SELECT.from, model)
685
- const _ignoredColumns = Object.values(target.elements ?? {})
686
- .filter(element => element['@cds.api.ignore'])
687
- .map(element => element.name)
688
- _validateQuery(x.SELECT, _ignoredColumns, target, x.SELECT.one, model)
680
+ _validateQuery(x.SELECT, target, x.SELECT.one, model)
689
681
  }
690
682
  }
691
683
 
692
684
  return _aliases
693
685
  }
694
686
 
695
- function _validateQuery(SELECT, ignoredColumns, target, isOne, model) {
687
+ function _validateQuery(SELECT, target, isOne, model) {
696
688
  const aliases = []
689
+
697
690
  if (SELECT.from.SELECT) {
698
691
  const { target } = targetFromPath(SELECT.from.SELECT.from, model)
699
- const _ignoredColumns = Object.values(target.elements ?? {})
700
- .filter(element => element['@cds.api.ignore'])
701
- .map(element => element.name)
702
- const subselectAliases = _validateQuery(SELECT.from.SELECT, _ignoredColumns, target, SELECT.from.SELECT.one, model)
692
+ const subselectAliases = _validateQuery(SELECT.from.SELECT, target, SELECT.from.SELECT.one, model)
703
693
  aliases.push(...subselectAliases)
704
694
  }
705
695
 
706
- const columnAliases = _validateXpr(SELECT.columns, ignoredColumns, target, isOne, model)
696
+ const columnAliases = _validateXpr(SELECT.columns, target, isOne, model)
707
697
  aliases.push(...columnAliases)
708
- _validateXpr(SELECT.orderBy, ignoredColumns, target, isOne, model, aliases)
709
- _validateXpr(SELECT.where, ignoredColumns, target, isOne, model, aliases)
710
- _validateXpr(SELECT.groupBy, ignoredColumns, target, isOne, model, aliases)
711
- _validateXpr(SELECT.having, ignoredColumns, target, isOne, model, aliases)
698
+
699
+ _validateXpr(SELECT.orderBy, target, isOne, model, aliases)
700
+ _validateXpr(SELECT.where, target, isOne, model, aliases)
701
+ _validateXpr(SELECT.groupBy, target, isOne, model, aliases)
702
+ _validateXpr(SELECT.having, target, isOne, model, aliases)
703
+
712
704
  return aliases
713
705
  }
714
706
 
@@ -773,12 +765,8 @@ module.exports = (cqn, model, namespace, protocol) => {
773
765
  _processColumns(cqn, current, protocol)
774
766
 
775
767
  if (target) {
776
- const ignoredColumns = Object.values(target.elements ?? {})
777
- .filter(element => element['@cds.api.ignore'])
778
- .map(element => element.name)
779
-
780
768
  // validate whether only known properties are used in query options
781
- _validateQuery(cqn.SELECT, ignoredColumns, target, one, model)
769
+ _validateQuery(cqn.SELECT, target, one, model)
782
770
  }
783
771
 
784
772
  return cqn
@@ -278,9 +278,11 @@ const skipToken = (token, cqn) => {
278
278
  const calculateLocationHeader = (target, srv, result) => {
279
279
  const targetName = target.name.replace(`${srv.definition.name}.`, '')
280
280
  const filteredKeys = [...target.keys].filter(k => !k.isAssociation).map(k => k.name)
281
+ if (!filteredKeys.every(k => k in result)) return
282
+
281
283
  const keyValuePairs = filteredKeys.reduce((acc, key) => {
282
284
  const value = result[key]
283
- if (value === undefined) return
285
+ if (value === undefined) return acc
284
286
  if (Buffer.isBuffer(value)) {
285
287
  acc[key] = value.toString('base64')
286
288
  } else {
@@ -290,7 +292,6 @@ const calculateLocationHeader = (target, srv, result) => {
290
292
  }
291
293
  return acc
292
294
  }, {})
293
- if (!keyValuePairs) return
294
295
  let keys
295
296
  const entries = Object.entries(keyValuePairs)
296
297
  if (entries.length === 1) {
@@ -1,5 +1,3 @@
1
- const cds = require('../../_runtime/cds')
2
-
3
1
  const getTemplate = require('../../_runtime/common/utils/template')
4
2
 
5
3
  const _addEtags = (row, key) => {
@@ -38,18 +36,15 @@ const _processorFn = elementInfo => {
38
36
  const _pick = element => {
39
37
  const categories = []
40
38
  if (element['@odata.etag']) categories.push('@odata.etag')
41
- if (element['@cds.api.ignore']) categories.push('@cds.api.ignore')
39
+ if (element['@cds.api.ignore'] && !element.isAssociation) categories.push('@cds.api.ignore')
42
40
  if (element._type === 'cds.Binary') categories.push('binary')
43
41
  if (element.items) categories.push('array')
44
42
  if (categories.length) return { categories }
45
43
  }
46
44
 
47
- module.exports = function postProcess(target, service, result, isMinimal) {
45
+ module.exports = function postProcess(target, model, result, isMinimal) {
48
46
  if (!result) return
49
47
 
50
- let { model } = service
51
- if (service.isExtensible) model = cds.context?.model || model
52
-
53
48
  if (!model.definitions[target.name]) {
54
49
  if (model.definitions[target.items?.type]) target = target.items
55
50
  else return
@@ -57,7 +52,7 @@ module.exports = function postProcess(target, service, result, isMinimal) {
57
52
 
58
53
  const cacheKey = isMinimal ? 'postProcessMinimal' : 'postProcess'
59
54
  const options = { pick: _pick, ignore: isMinimal ? el => el.isAssociation : undefined }
60
- const template = getTemplate(cacheKey, service, target, options)
55
+ const template = getTemplate(cacheKey, { model }, target, options)
61
56
 
62
57
  if (template.elements.size === 0) return
63
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.2.3",
3
+ "version": "8.3.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -1,2 +0,0 @@
1
- // REVISIT: remove with cds^8
2
- module.exports = require('../../../common/utils/compareJson')