@sap/cds 7.2.0 → 7.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 (63) hide show
  1. package/CHANGELOG.md +174 -126
  2. package/README.md +1 -1
  3. package/apis/connect.d.ts +1 -1
  4. package/apis/core.d.ts +6 -4
  5. package/apis/serve.d.ts +1 -1
  6. package/apis/services.d.ts +51 -31
  7. package/apis/test.d.ts +24 -10
  8. package/bin/serve.js +4 -3
  9. package/common.cds +4 -4
  10. package/lib/auth/ias-auth.js +7 -8
  11. package/lib/compile/cdsc.js +5 -7
  12. package/lib/compile/etc/csv.js +22 -11
  13. package/lib/dbs/cds-deploy.js +1 -2
  14. package/lib/env/cds-env.js +26 -20
  15. package/lib/env/defaults.js +4 -3
  16. package/lib/env/schema.js +9 -0
  17. package/lib/i18n/localize.js +83 -77
  18. package/lib/index.js +6 -2
  19. package/lib/linked/classes.js +13 -13
  20. package/lib/plugins.js +41 -45
  21. package/lib/req/user.js +2 -2
  22. package/lib/srv/protocols/_legacy.js +0 -1
  23. package/lib/srv/protocols/odata-v4.js +4 -0
  24. package/lib/utils/axios.js +7 -1
  25. package/lib/utils/cds-test.js +140 -133
  26. package/lib/utils/cds-utils.js +1 -1
  27. package/lib/utils/check-version.js +6 -0
  28. package/lib/utils/data.js +19 -6
  29. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +20 -19
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +10 -1
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +1 -1
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -3
  33. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +0 -14
  34. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
  35. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +5 -2
  36. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/MetadataHandler.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/ServiceHandler.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -2
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -3
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -1
  41. package/libx/_runtime/common/composition/update.js +18 -2
  42. package/libx/_runtime/common/error/frontend.js +46 -34
  43. package/libx/_runtime/common/generic/auth/capabilities.js +33 -14
  44. package/libx/_runtime/common/generic/input.js +1 -1
  45. package/libx/_runtime/common/generic/paging.js +1 -0
  46. package/libx/_runtime/common/i18n/messages.properties +1 -0
  47. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
  48. package/libx/_runtime/db/query/update.js +48 -30
  49. package/libx/_runtime/fiori/lean-draft.js +23 -24
  50. package/libx/_runtime/hana/conversion.js +3 -2
  51. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
  52. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  53. package/libx/_runtime/remote/Service.js +11 -26
  54. package/libx/_runtime/remote/utils/client.js +3 -2
  55. package/libx/_runtime/remote/utils/data.js +5 -7
  56. package/libx/odata/{grammar.pegjs → grammar.peggy} +1 -1
  57. package/libx/odata/metadata.js +121 -0
  58. package/libx/odata/parser.js +1 -1
  59. package/libx/odata/service-document.js +61 -0
  60. package/libx/odata/utils.js +102 -48
  61. package/libx/rest/RestAdapter.js +2 -2
  62. package/libx/rest/middleware/error.js +1 -1
  63. package/package.json +1 -1
@@ -34,6 +34,7 @@ const _setHeaders = (defaultHeaders, req) => {
34
34
  }
35
35
 
36
36
  const _setCorrectValue = (el, data, params, kind) => {
37
+ if (data[el] === undefined) return "'undefined'"
37
38
  return typeof data[el] === 'object' && kind !== 'odata-v2'
38
39
  ? JSON.stringify(data[el])
39
40
  : formatVal(data[el], el, { elements: params }, kind)
@@ -109,8 +110,9 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
109
110
  def &&
110
111
  def.returns &&
111
112
  (def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
113
+ const { headers, data } = req
112
114
 
113
- return srv.send({ method: 'POST', path: `/${event}`, data: req.data, _binary: isBinary })
115
+ return srv.send({ method: 'POST', path: `/${event}`, headers, data, _binary: isBinary })
114
116
  }
115
117
 
116
118
  const url =
@@ -118,16 +120,17 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
118
120
  return srv.get(url)
119
121
  }
120
122
 
121
- const _sendV2RequestActionFunction = (srv, def, url) => {
123
+ const _sendV2RequestActionFunction = (srv, def, req, url) => {
124
+ const { headers } = req
122
125
  return def.kind === 'function'
123
- ? srv.send({ method: 'GET', path: url, _returnType: def.returns })
124
- : srv.send({ method: 'POST', path: url, data: {}, _returnType: def.returns })
126
+ ? srv.send({ method: 'GET', path: url, headers, _returnType: def.returns })
127
+ : srv.send({ method: 'POST', path: url, headers, data: {}, _returnType: def.returns })
125
128
  }
126
129
 
127
130
  const _handleV2ActionFunction = (srv, def, req, event, kind) => {
128
131
  const url =
129
132
  Object.keys(req.data).length > 0 ? _buildPartialUrlFunctions(`/${event}`, req.data, def.params, kind) : `/${event}`
130
- return _sendV2RequestActionFunction(srv, def, url)
133
+ return _sendV2RequestActionFunction(srv, def, req, url)
131
134
  }
132
135
 
133
136
  const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
@@ -147,7 +150,7 @@ const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
147
150
  }
148
151
 
149
152
  const url = `${`/${event}`}?${params.join('&')}`
150
- return _sendV2RequestActionFunction(srv, def, url)
153
+ return _sendV2RequestActionFunction(srv, def, req, url)
151
154
  }
152
155
 
153
156
  const _addHandlerActionFunction = (srv, def, target) => {
@@ -178,7 +181,6 @@ const resolvedTargetOfQuery = q => {
178
181
  }
179
182
 
180
183
  let logged
181
- let sdkLoggerDisabled
182
184
 
183
185
  const _resolveSelectionStrategy = options => {
184
186
  if (typeof options?.selectionStrategy !== 'string') return
@@ -226,22 +228,6 @@ class RemoteService extends cds.Service {
226
228
  'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
227
229
  )
228
230
  }
229
-
230
- // REVISIT: use cds.log's logger in cloud sdk
231
-
232
- // disable sdk logger if not in debug mode
233
- if (!LOG._debug && !sdkLoggerDisabled) {
234
- try {
235
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
236
- const sdkUtils = require('@sap-cloud-sdk/util')
237
- sdkUtils.setGlobalLogLevel('error')
238
-
239
- // disable sdk logger once
240
- sdkLoggerDisabled = true
241
- } catch (err) {
242
- /* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
243
- }
244
- }
245
231
  } else if ([...this.entities].length || [...this.operations].length) {
246
232
  throw new Error(`No credentials configured for "${this.name}".`)
247
233
  }
@@ -262,6 +248,8 @@ class RemoteService extends cds.Service {
262
248
  }
263
249
 
264
250
  this.on('*', async (req, next) => {
251
+ const { query } = req
252
+ if (!query && !(typeof req.path === 'string')) return next()
265
253
  // early validation on first request for use case without remote API
266
254
  // ideally, that's done on bootstrap of the remote service
267
255
  if (typeof this.destination === 'object' && !this.destination.url)
@@ -269,9 +257,6 @@ class RemoteService extends cds.Service {
269
257
  if (this._resilienceMiddlewares && !this._resilienceMiddlewares.timeout)
270
258
  this._resilienceMiddlewares.timeout = cloudSdkResilience().timeout(this.requestTimeout)
271
259
 
272
- const { query } = req
273
- if (!query && !(typeof req.path === 'string')) return next()
274
-
275
260
  const resolvedTarget = resolvedTargetOfQuery(query) || getTransition(req.target, this).target
276
261
  const reqOptions = getReqOptions(req, query, this)
277
262
  reqOptions.headers = _setHeaders(reqOptions.headers, req)
@@ -427,12 +427,13 @@ const _stringToReqOptions = (query, data, target) => {
427
427
  return reqOptions
428
428
  }
429
429
 
430
- const _pathToReqOptions = (method, path, data, target) => {
430
+ const _pathToReqOptions = (method, path, data, target, srvName) => {
431
431
  let url = path
432
432
  if (!url.startsWith('/')) {
433
433
  // extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
434
434
  const parts = path.match(/([\w.]*)([\W.]*)(.*)/)
435
435
  if (!parts) url = '/' + path.match(/\w*$/)[0]
436
+ else if (url.startsWith(srvName)) url = '/' + parts[1].replace(srvName + '.', '') + parts[2] + parts[3]
436
437
  else url = '/' + parts[1].match(/\w*$/)[0] + parts[2] + parts[3]
437
438
 
438
439
  // normalize in case parts[2] already starts with /
@@ -459,7 +460,7 @@ const getReqOptions = (req, query, service) => {
459
460
  ? _cqnToReqOptions(query, service, req)
460
461
  : typeof query === 'string'
461
462
  ? _stringToReqOptions(query, req.data, req.target)
462
- : _pathToReqOptions(req.method, req.path, req.data, req.target)
463
+ : _pathToReqOptions(req.method, req.path, req.data, req.target, service.name)
463
464
 
464
465
  if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
465
466
  req.reject(501, 'Lambda expressions are not supported in OData v2')
@@ -66,12 +66,9 @@ const _convertActionFuncResponse = (returnType, convertValueFn) => data => {
66
66
 
67
67
  // eslint-disable-next-line complexity
68
68
  const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
69
- if (value == null) {
70
- return value
71
- }
69
+ if (value == null) return value
72
70
 
73
71
  const type = _elementType(element)
74
-
75
72
  if (type === 'cds.Boolean') {
76
73
  if (value === 'true') {
77
74
  value = true
@@ -119,18 +116,19 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
119
116
 
120
117
  return value
121
118
  }
119
+
122
120
  const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
123
121
 
124
122
  const _convertPayloadValue = (value, element) => {
125
- const type = _elementType(element)
123
+ if (value == null) return value
126
124
 
127
125
  // see https://www.odata.org/documentation/odata-version-2-0/json-format/
128
- if (value == null) return value
126
+ const type = _elementType(element)
129
127
  switch (type) {
130
128
  case 'cds.Time':
131
129
  return value.match(/^(PT)([H,M,S,0-9])*$/) ? value : _PT(value.split(':'))
132
130
  case 'cds.Decimal':
133
- return typeof value === 'string' ? value : new String(value)
131
+ return typeof value === 'string' ? value : `${value}`
134
132
  case 'cds.Date':
135
133
  case 'cds.DateTime':
136
134
  return `/Date(${new Date(value).getTime()})/`
@@ -33,7 +33,7 @@
33
33
  const stack = []
34
34
  let SELECT, count
35
35
  const TECHNICAL_OPTS = ['$value'] // odata parts to be handled somewhere else
36
- // we keep that here to allow for usage in https://pegjs.org/online
36
+ // we keep that here to allow for usage in https://peggyjs.org/online
37
37
  const safeNumber =
38
38
  options.safeNumber ||
39
39
  function (str) {
@@ -0,0 +1,121 @@
1
+ const cds = require('../../lib')
2
+ const LOG = cds.log('odata')
3
+ const crypto = require('crypto')
4
+
5
+ const _requestedFormat = (queryOption, header) => {
6
+ if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
7
+ if (header) {
8
+ const jsonIndex = header.indexOf('application/json')
9
+ if (jsonIndex === -1) return 'xml'
10
+ const xmlIndex = header.indexOf('application/xml')
11
+ if (xmlIndex === -1) return 'json'
12
+ return jsonIndex < xmlIndex ? 'json' : 'xml'
13
+ }
14
+ return 'xml'
15
+ }
16
+
17
+ const _metadataFromFile = async srv => {
18
+ const fs = require('fs')
19
+ const filePath = cds.root + `/srv/odata/v4/${srv.name}.xml`
20
+ let exists
21
+ try {
22
+ exists = !(await fs.promises.access(filePath, fs.constants.F_OK))
23
+ } catch (e) {
24
+ LOG._debug && LOG.debug(`No metadata file found for service ${srv.name} at ${filePath}`)
25
+ }
26
+ if (exists) {
27
+ const file = await fs.promises.readFile(filePath)
28
+ return file.toString()
29
+ }
30
+ }
31
+
32
+ const normalize_header = value => {
33
+ return value.split(',').map(str => str.trim())
34
+ }
35
+
36
+ const validate_etag = (ifNoneMatch, etag) => {
37
+ const ifNoneMatchEtags = normalize_header(ifNoneMatch)
38
+ return ifNoneMatchEtags.includes(etag) || ifNoneMatchEtags.includes('*')
39
+ }
40
+
41
+ const generateEtag = s => {
42
+ return `W/"${crypto.createHash('sha256').update(s).digest('base64')}"`
43
+ }
44
+
45
+ const odata_error = (code, message) => ({ error: { code, message } })
46
+
47
+ module.exports = srv =>
48
+ async function metadata(req, res, next) {
49
+ if (req.path === '/$metadata') {
50
+ if (req.method !== 'GET')
51
+ return res
52
+ .status(405)
53
+ .json(odata_error('METHOD_NOT_ALLOWED', `Method ${req.method} not allowed for $metadata.`))
54
+
55
+ const tenant = cds.context.tenant
56
+ const locale = cds.context.locale
57
+ const format = _requestedFormat(req.query['$format'], req.headers['accept'])
58
+
59
+ // REVISIT: edm(x) and etag cache is only evicted with model
60
+ const csnService = (cds.context.model || cds.model).definitions[srv.name]
61
+ const metadataCache = (csnService.metadataCache = csnService.metadataCache || { jsonEtag: {}, xmlEtag: {} })
62
+
63
+ const etag = format === 'json' ? metadataCache.jsonEtag?.[locale] : metadataCache.xmlEtag?.[locale]
64
+
65
+ if (req.headers['if-none-match']) {
66
+ if (etag) {
67
+ const unchanged = validate_etag(req.headers['if-none-match'], etag)
68
+ if (unchanged) {
69
+ res.set('Etag', etag)
70
+ return res.status(304).end()
71
+ }
72
+ }
73
+ }
74
+
75
+ const { 'cds.xt.ModelProviderService': mps } = cds.services
76
+ if (mps) {
77
+ if (format === 'json')
78
+ res
79
+ .status(400)
80
+ .json(
81
+ odata_error(
82
+ 'UNSUPPORTED_METADATA_TYPE',
83
+ 'JSON metadata is not supported if cds.requires.extensibilty: true.'
84
+ )
85
+ )
86
+
87
+ try {
88
+ const edmx = await mps.getEdmx({ tenant, model: srv.model, service: srv.definition.name, locale })
89
+ metadataCache.xmlEtag[locale] = generateEtag(edmx)
90
+ res.set('Content-Type', 'application/xml')
91
+ res.send(edmx)
92
+ } catch (e) {
93
+ if (LOG._error) {
94
+ e.message = 'Unable to get EDMX for tenant ' + tenant + ' due to error: ' + e.message
95
+ LOG.error(e)
96
+ }
97
+
98
+ return res.status(503).json(odata_error('SERVICE_UNAVAILABLE', 'Service unavailable'))
99
+ }
100
+ }
101
+
102
+ if (format === 'json') {
103
+ const edm =
104
+ metadataCache.edm || (metadataCache.edm = cds.compile.to.edm(srv.model, { service: srv.definition.name }))
105
+ const localized = cds.localize(srv.model, locale, edm)
106
+ metadataCache.jsonEtag[locale] = generateEtag(localized)
107
+ return res.json(JSON.parse(localized))
108
+ }
109
+
110
+ const edmx =
111
+ metadataCache.edmx ||
112
+ (await _metadataFromFile(srv)) ||
113
+ (metadataCache.edmx = cds.compile.to.edmx(srv.model, { service: srv.definition.name }))
114
+ const localized = cds.localize(srv.model, locale, edmx)
115
+ metadataCache.xmlEtag[locale] = generateEtag(localized)
116
+ res.set('Etag', metadataCache.xmlEtag[locale])
117
+ res.set('Content-Type', 'application/xml')
118
+ return res.send(localized)
119
+ }
120
+ return next()
121
+ }