@sap/cds 8.7.2 → 8.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 (58) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/_i18n/i18n.properties +3 -0
  3. package/_i18n/i18n_cs.properties +6 -6
  4. package/_i18n/i18n_de.properties +3 -0
  5. package/_i18n/i18n_en.properties +3 -0
  6. package/_i18n/i18n_es.properties +3 -0
  7. package/_i18n/i18n_fr.properties +3 -0
  8. package/_i18n/i18n_it.properties +3 -0
  9. package/_i18n/i18n_ja.properties +3 -0
  10. package/_i18n/i18n_pl.properties +7 -4
  11. package/_i18n/i18n_pt.properties +3 -0
  12. package/_i18n/i18n_ru.properties +3 -0
  13. package/app/index.js +2 -30
  14. package/lib/compile/parse.js +1 -1
  15. package/lib/env/cds-env.js +1 -1
  16. package/lib/env/cds-requires.js +16 -9
  17. package/lib/env/schemas/cds-package.js +1 -1
  18. package/lib/env/schemas/cds-rc.js +17 -4
  19. package/lib/index.js +1 -1
  20. package/lib/ql/SELECT.js +6 -1
  21. package/lib/ql/cds.ql-predicates.js +2 -1
  22. package/lib/req/request.js +5 -2
  23. package/lib/req/validate.js +4 -2
  24. package/lib/srv/bindings.js +31 -20
  25. package/lib/srv/cds-connect.js +1 -1
  26. package/lib/srv/middlewares/auth/mocked-users.js +1 -0
  27. package/lib/srv/protocols/okra.js +5 -7
  28. package/lib/srv/srv-dispatch.js +0 -5
  29. package/lib/test/cds-test.js +34 -6
  30. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -0
  31. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  32. package/libx/_runtime/common/generic/auth/service.js +2 -2
  33. package/libx/_runtime/common/generic/auth/utils.js +2 -1
  34. package/libx/_runtime/common/generic/input.js +1 -1
  35. package/libx/_runtime/common/utils/binary.js +1 -35
  36. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -8
  37. package/libx/_runtime/fiori/lean-draft.js +2 -4
  38. package/libx/common/utils/path.js +1 -5
  39. package/libx/common/utils/streaming.js +76 -0
  40. package/libx/odata/middleware/create.js +5 -1
  41. package/libx/odata/middleware/delete.js +1 -1
  42. package/libx/odata/middleware/operation.js +48 -4
  43. package/libx/odata/middleware/read.js +1 -1
  44. package/libx/odata/middleware/stream.js +29 -101
  45. package/libx/odata/middleware/update.js +1 -1
  46. package/libx/odata/parse/afterburner.js +21 -1
  47. package/libx/odata/parse/grammar.peggy +108 -26
  48. package/libx/odata/parse/multipartToJson.js +17 -10
  49. package/libx/odata/parse/parser.js +1 -1
  50. package/libx/odata/utils/metadata.js +28 -5
  51. package/libx/odata/utils/normalizeTimeData.js +11 -8
  52. package/libx/rest/RestAdapter.js +2 -16
  53. package/libx/rest/middleware/operation.js +38 -18
  54. package/libx/rest/middleware/parse.js +5 -25
  55. package/libx/rest/post-processing.js +33 -0
  56. package/libx/rest/pre-processing.js +38 -0
  57. package/package.json +1 -1
  58. package/libx/common/utils/index.js +0 -5
@@ -1,10 +1,12 @@
1
1
  const cds = require('../../_runtime/cds')
2
+ const getError = require('../../_runtime/common/error')
2
3
 
3
4
  const { checkStatic } = require('../../_runtime/cds-services/util/assert')
4
- const getError = require('../../_runtime/common/error')
5
+ const { Readable } = require('node:stream')
5
6
 
6
7
  // REVISIT: strict or relaxed type checkers?
7
8
  const typeCheckers = require('../../common/assert/type-strict')
9
+ const streaming = require('../../common/utils/streaming')
8
10
 
9
11
  // REVISIT: use i18n
10
12
  const _enrichErrorDetails = (isPrimitive, error) => {
@@ -74,21 +76,40 @@ module.exports = adapter => {
74
76
 
75
77
  return async function operation(req, res) {
76
78
  const { _query: query, _operation: operation, _data: data, _params: params } = req
77
-
78
- let result
79
-
80
- result = await service.dispatch(
81
- adapter.request4(
82
- Object.assign(
83
- query
84
- ? { query, event: operation.name }
85
- : { event: operation.name.replace(`${service.definition.name}.`, '') },
86
- { data, params, req, res }
87
- )
79
+ const request = adapter.request4(
80
+ Object.assign(
81
+ query // REVISIT: when is a query given, and when not? -> bound vs unbound? -> obsolete with req.subject? -> deprecated?
82
+ ? { event: operation.name, query } // REVISIT: why don't we need a similar .replace as below here?
83
+ : { event: operation.name.replace(`${service.definition.name}.`, '') }, // REVISIT: why is this .replace needed?
84
+ { data, params, req, res } // REVISIT: req and res are not part of the public API !!!
88
85
  )
89
86
  )
90
87
 
91
- if (!operation.returns) return { status: 204 }
88
+ let result = await service.dispatch(request)
89
+
90
+ if (!operation.returns || result == null) return { status: 204 }
91
+
92
+ const _isStream = result instanceof Readable || (operation.returns._type === 'cds.LargeBinary' && 'value' in result)
93
+ if (_isStream) {
94
+ const stream = streaming.getReadable(result)
95
+ if (!stream) return res.sendStatus(204)
96
+
97
+ const { mimetype, filename, disposition } = streaming.collectStreamMetadata(result, operation, query)
98
+ streaming.validateMimetypeIsAcceptedOrThrow(req.headers, mimetype)
99
+
100
+ if (mimetype && !res.get('Content-Type'))
101
+ res.set('Content-Type', mimetype)
102
+
103
+ if (filename && !res.get('Content-Disposition'))
104
+ res.set('Content-Disposition', `${disposition}; filename="${filename}"`)
105
+ // NOTE: We should NOT use encodeURIComponent here, as it is not needed for the filename in the Content-Disposition header
106
+ // and it would break the filename in the header if it contains special characters like
107
+ // e.g. German umlauts (ä, ö, ü) or spaces
108
+ // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
109
+
110
+ return stream.pipe(res) && {} // REVISIT: strange caller expects this
111
+ }
112
+
92
113
 
93
114
  // REVISIT: do not use from old rest adapter
94
115
  // REVISIT: new impl should return instead of throwing to avoid try catch
@@ -97,11 +118,10 @@ module.exports = adapter => {
97
118
  // set content-type header to text/plain for returned primitive data types, except for boolean
98
119
  const returnType = operation.returns._type
99
120
  if (
100
- !res.get('content-type') &&
101
- !operation.returns.items &&
102
- returnType &&
103
121
  cds.builtin.types[returnType] &&
104
- returnType !== 'cds.Boolean'
122
+ !operation.returns.items &&
123
+ returnType !== 'cds.Boolean' &&
124
+ !res.get('content-type')
105
125
  ) {
106
126
  res.set('Content-Type', 'text/plain')
107
127
  }
@@ -112,7 +132,7 @@ module.exports = adapter => {
112
132
  res.set('Content-Type', 'application/json')
113
133
  }
114
134
 
115
- // REVISIT: still needed?
135
+ // REVISIT: still needed? // REVISIT: indeed!
116
136
  if (!operation.returns.items && Array.isArray(result)) result = result[0]
117
137
 
118
138
  if (result === undefined) return { status: 204 }
@@ -1,31 +1,11 @@
1
1
  const cds = require('../../_runtime/cds')
2
2
  const { INSERT, SELECT, UPDATE, DELETE } = cds.ql
3
3
 
4
- const { getKeysAndParamsFromPath } = require('../../common/utils')
4
+ const { getKeysAndParamsFromPath } = require('../../common/utils/path')
5
5
 
6
- const { base64ToBuffer } = require('../../_runtime/common/utils/binary')
6
+ const { preProcessData } = require('../pre-processing')
7
7
  const { convertStructured } = require('../../_runtime/common/utils/ucsn')
8
- const getTemplate = require('../../_runtime/common/utils/template')
9
8
 
10
- const { checkStaticElementByKey } = require('../../_runtime/cds-services/util/assert')
11
-
12
- const _processorFn = errors => {
13
- return ({ row, key, plain: categories, target }) => {
14
- // REVISIT move validation to generic asserter => see PR 717
15
- if (categories['static_validation'] && row[key] != null) {
16
- const validations = checkStaticElementByKey(target, key, row[key])
17
- errors.push(...validations)
18
- }
19
- }
20
- }
21
-
22
- const _picker = element => {
23
- const categories = {}
24
- if (Array.isArray(element)) return
25
- if (element._isStructured || element.isAssociation || element.items) return
26
- categories['static_validation'] = true
27
- return categories
28
- }
29
9
 
30
10
  const _cache = req => `rest-input;skip-key-validation:${req.method !== 'POST'}`
31
11
 
@@ -149,14 +129,14 @@ module.exports = adapter => {
149
129
  convertStructured(service, operation || definition, payload, {
150
130
  cleanupStruct: cds.env.features.rest_struct_data
151
131
  })
152
- const template = getTemplate(_cache(req), service, definition, { pick: _picker })
153
- template.process(payload, _processorFn(errs))
132
+ preProcessData(payload, service, definition, _cache, errs)
154
133
  if (errs.length) {
155
134
  if (errs.length === 1) throw errs[0]
156
135
  throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
157
136
  }
137
+ } else {
138
+ preProcessData(payload, service, definition)
158
139
  }
159
- base64ToBuffer(payload, service, definition)
160
140
  req._data = payload
161
141
  }
162
142
  }
@@ -0,0 +1,33 @@
1
+ const getTemplate = require('../_runtime/common/utils/template')
2
+ const _picker = element => {
3
+ const categories = []
4
+ if (Array.isArray(element)) return
5
+ if (element.type === 'cds.Binary' || element.type === 'cds.LargeBinary') categories.push('convert_binary')
6
+ if (element['@cds.api.ignore'] && !element.isAssociation) categories.push('@cds.api.ignore')
7
+ if (categories.length) return { categories }
8
+ }
9
+
10
+ const _processorFn = elementInfo => {
11
+ const { row, key, plain } = elementInfo
12
+ if (typeof row !== 'object') return
13
+ for (const category of plain.categories) {
14
+ switch (category) {
15
+ case 'convert_binary':
16
+ if (row[key] != null && Buffer.isBuffer(row[key])) row[key] = row[key].toString('base64')
17
+ break
18
+ case '@cds.api.ignore':
19
+ delete row[key]
20
+ break
21
+ // no default
22
+ }
23
+ }
24
+ }
25
+
26
+ const postProcessData = (data, srv, definition) => {
27
+ const template = getTemplate('rest-post-process', srv, definition, { pick: _picker })
28
+ template.process(data, _processorFn)
29
+ }
30
+
31
+ module.exports = {
32
+ postProcessData
33
+ }
@@ -0,0 +1,38 @@
1
+ const getTemplate = require('../_runtime/common/utils/template')
2
+ const { checkStaticElementByKey } = require('../_runtime/cds-services/util/assert')
3
+ const _picker = element => {
4
+ const categories = []
5
+ if (Array.isArray(element)) return
6
+ if (element.type === 'cds.Binary' || element.type === 'cds.LargeBinary') categories.push('convert_binary')
7
+ if (!(element._isStructured || element.isAssociation || element.items)) categories.push('static_validation')
8
+ if (categories.length) return { categories }
9
+ }
10
+
11
+ const _processorFn = errors => elementInfo => {
12
+ const { row, key, plain, target } = elementInfo
13
+ if (typeof row !== 'object') return
14
+ if (row[key] == null) return
15
+ for (const category of plain.categories) {
16
+ switch (category) {
17
+ case 'convert_binary':
18
+ if (typeof row[key] === 'string') row[key] = Buffer.from(row[key], 'base64')
19
+ break
20
+ case 'static_validation':
21
+ if (errors) { //> errors collector is only provided in case of !cds.env.features.cds_validate
22
+ // REVISIT move validation to generic asserter => see PR 717
23
+ const validations = checkStaticElementByKey(target, key, row[key])
24
+ errors.push(...validations)
25
+ }
26
+ break
27
+ }
28
+ }
29
+ }
30
+
31
+ const preProcessData = (data, srv, definition, usecase = 'rest-pre-process', errors) => {
32
+ const template = getTemplate(usecase, srv, definition, { pick: _picker })
33
+ template.process(data, _processorFn(errors))
34
+ }
35
+
36
+ module.exports = {
37
+ preProcessData
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.7.2",
3
+ "version": "8.8.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -1,5 +0,0 @@
1
- const { getKeysAndParamsFromPath } = require('./path')
2
-
3
- module.exports = {
4
- getKeysAndParamsFromPath
5
- }