@sap/cds 7.6.4 → 7.7.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 (97) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/_i18n/i18n.properties +3 -0
  3. package/app/index.js +14 -8
  4. package/bin/serve.js +51 -19
  5. package/common.cds +16 -0
  6. package/lib/auth/ias-auth.js +2 -2
  7. package/lib/auth/index.js +1 -1
  8. package/lib/auth/jwt-auth.js +1 -1
  9. package/lib/compile/cdsc.js +23 -11
  10. package/lib/compile/for/nodejs.js +2 -2
  11. package/lib/compile/for/odata.js +4 -0
  12. package/lib/compile/load.js +7 -2
  13. package/lib/compile/to/sql.js +3 -0
  14. package/lib/dbs/cds-deploy.js +197 -220
  15. package/lib/env/defaults.js +2 -1
  16. package/lib/index.js +8 -2
  17. package/lib/linked/types.js +1 -0
  18. package/lib/log/format/json.js +4 -1
  19. package/lib/plugins.js +2 -2
  20. package/lib/ql/SELECT.js +8 -8
  21. package/lib/req/context.js +22 -13
  22. package/lib/req/request.js +10 -4
  23. package/lib/srv/cds-connect.js +9 -3
  24. package/lib/srv/cds-serve.js +5 -3
  25. package/lib/srv/middlewares/ctx-model.js +1 -1
  26. package/lib/srv/protocols/odata-v4.js +38 -9
  27. package/lib/srv/srv-api.js +98 -140
  28. package/lib/srv/srv-models.js +2 -2
  29. package/lib/srv/srv-tx.js +1 -0
  30. package/lib/utils/cds-utils.js +32 -23
  31. package/lib/utils/data.js +1 -1
  32. package/lib/utils/tar.js +1 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
  41. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
  43. package/libx/_runtime/cds-services/util/assert.js +50 -240
  44. package/libx/_runtime/cds.js +5 -0
  45. package/libx/_runtime/common/aspects/any.js +53 -45
  46. package/libx/_runtime/common/generic/input.js +14 -10
  47. package/libx/_runtime/common/generic/paging.js +1 -1
  48. package/libx/_runtime/common/utils/cqn.js +1 -1
  49. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  50. package/libx/_runtime/common/utils/keys.js +1 -1
  51. package/libx/_runtime/common/utils/quotingStyles.js +1 -1
  52. package/libx/_runtime/common/utils/resolveStructured.js +4 -1
  53. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
  54. package/libx/_runtime/common/utils/stream.js +2 -16
  55. package/libx/_runtime/common/utils/streamProp.js +16 -6
  56. package/libx/_runtime/common/utils/ucsn.js +1 -0
  57. package/libx/_runtime/db/expand/expandCQNToJoin.js +1 -1
  58. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  59. package/libx/_runtime/db/utils/columns.js +6 -1
  60. package/libx/_runtime/fiori/generic/activate.js +11 -3
  61. package/libx/_runtime/fiori/generic/edit.js +8 -2
  62. package/libx/_runtime/fiori/lean-draft.js +94 -30
  63. package/libx/_runtime/hana/execute.js +2 -5
  64. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +12 -22
  65. package/libx/_runtime/messaging/service.js +6 -2
  66. package/libx/common/assert/index.js +232 -0
  67. package/libx/common/assert/type.js +109 -0
  68. package/libx/common/assert/utils.js +125 -0
  69. package/libx/common/assert/validation.js +109 -0
  70. package/libx/odata/index.js +5 -5
  71. package/libx/odata/middleware/create.js +83 -0
  72. package/libx/odata/middleware/delete.js +38 -0
  73. package/libx/odata/middleware/error.js +8 -0
  74. package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
  75. package/libx/odata/middleware/operation.js +78 -0
  76. package/libx/odata/middleware/parse.js +11 -0
  77. package/libx/odata/{read.js → middleware/read.js} +42 -20
  78. package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
  79. package/libx/odata/middleware/stream.js +237 -0
  80. package/libx/odata/middleware/update.js +165 -0
  81. package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
  82. package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
  83. package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
  84. package/libx/odata/{utils.js → utils/index.js} +95 -9
  85. package/libx/outbox/index.js +2 -1
  86. package/libx/rest/RestAdapter.js +0 -1
  87. package/libx/rest/middleware/operation.js +6 -4
  88. package/libx/rest/middleware/parse.js +20 -2
  89. package/package.json +1 -1
  90. package/server.js +43 -71
  91. package/libx/odata/create.js +0 -44
  92. package/libx/odata/delete.js +0 -25
  93. package/libx/odata/error.js +0 -12
  94. package/libx/odata/update.js +0 -110
  95. /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
  96. /package/libx/odata/{parser.js → parse/parser.js} +0 -0
  97. /package/libx/odata/{result.js → utils/result.js} +0 -0
package/lib/utils/tar.js CHANGED
@@ -11,7 +11,7 @@ const _resolve = (...x) => path.resolve (cds.root,...x)
11
11
  // tar does not work properly on Windows (by npm/jest tests) w/o this change
12
12
  const win = path => {
13
13
  if (!path) return path
14
- if (typeof path === 'string') return path.replace('C:', '//localhost/c$')
14
+ if (typeof path === 'string') return path.replace('C:', '//localhost/c$').replace(/\\+/g, '/')
15
15
  if (Array.isArray(path)) return path.map(el => win(el))
16
16
  }
17
17
 
@@ -31,9 +31,7 @@ const metadata = service => {
31
31
  // REVISIT: remove check later
32
32
  if (mpSupportsEmptyLocale()) {
33
33
  // If no extensibility nor fts, do not provide model to mtxs
34
- const modelNeeded =
35
- cds.env.requires.extensibility ||
36
- (cds.env.requires.toggles && Object.keys(cds.context.features || {}).length)
34
+ const modelNeeded = cds.env.requires.extensibility || cds.context.features?.given
37
35
  edmx = await mps.getEdmx({ tenant, model: modelNeeded && service.model, service: service.definition.name })
38
36
  const extBundle = cds.env.requires.extensibility && (await mps.getI18n({ tenant, locale }))
39
37
  edmx = cds.localize(service.model, locale, edmx, extBundle)
@@ -23,7 +23,6 @@ const getError = require('../../../../common/error')
23
23
  const { getSapMessages } = require('../../../../common/error/frontend')
24
24
  const { getPageSize, commonGenericPaging } = require('../../../../common/generic/paging')
25
25
  const { handler: commonGenericSorting } = require('../../../../common/generic/sorting')
26
- const { transformRedirectProperties } = require('../../../../common/utils/stream')
27
26
  const { getTransition } = require('../../../../common/utils/resolveView')
28
27
  const { Readable } = require('stream')
29
28
 
@@ -481,7 +480,6 @@ const _readAndTransform = (tx, req, odataReq) => {
481
480
  }
482
481
 
483
482
  const _postProcess = (odataReq, req, odataRes, service, result) => {
484
- transformRedirectProperties(req, service, result.value)
485
483
  const functionReturnType = getActionOrFunctionReturnType(
486
484
  odataReq.getUriInfo().getPathSegments(),
487
485
  service.model.definitions
@@ -125,6 +125,21 @@ const _hasEtag = target => {
125
125
  return target._etag
126
126
  }
127
127
 
128
+ const _getStructValue = (prop, result, segments) => {
129
+ const path = [prop]
130
+ for (let i = segments.length - 2; i >= 0; i--) {
131
+ const segment = segments[i]
132
+ if (segment.getKind() === 'COMPLEX.PROPERTY') {
133
+ path.unshift(segment.getProperty().getName())
134
+ } else break
135
+ }
136
+
137
+ let res = result
138
+ path.forEach(p => (res = res[p]))
139
+
140
+ return { value: res }
141
+ }
142
+
128
143
  /**
129
144
  * The handler that will be registered with odata-v4.
130
145
  *
@@ -201,7 +216,9 @@ const update = service => {
201
216
  if (err) next(err)
202
217
  else if (primitive && result) {
203
218
  const prop = odataReq.getUriInfo().getLastSegment().getProperty().getName()
204
- const res = { value: result[prop] }
219
+ const res = cds.env.effective.odata.structs
220
+ ? _getStructValue(prop, result, odataReq.getUriInfo().getPathSegments())
221
+ : { value: result[prop] }
205
222
  for (const k of Object.keys(result).filter(k => k.match(/^\*/))) res[k] = result[k]
206
223
  next(null, res)
207
224
  } else next(null, toODataResult(result, req))
@@ -9,7 +9,7 @@ const readToCQN = require('./readToCQN')
9
9
  const updateToCQN = require('./updateToCQN')
10
10
  const createToCQN = require('./createToCQN')
11
11
  const deleteToCQN = require('./deleteToCQN')
12
- const parseToCqn = require('../../../../../odata/parseToCqn')
12
+ const parseToCqn = require('../../../../../odata/parse/parseToCqn')
13
13
 
14
14
  /**
15
15
  * This method transforms an odata request into a CQN object.
@@ -187,18 +187,22 @@ const _parsePrimitiveValue = (edmRef, value) => {
187
187
  const getSegmentKeyValue = segmentParam => {
188
188
  const edmRef = segmentParam.getEdmRef()
189
189
  const keyName = edmRef.getName()
190
+ let val
190
191
  if (segmentParam.getAliasValue()) {
191
192
  const value = segmentParam.getAliasValue()
192
193
  // must be JSON or a string according to
193
194
  // https://docs.oasis-open.org/odata/odata/v4.01/os/part2-url-conventions/odata-v4.01-os-part2-url-conventions.html#sec_ComplexandCollectionLiterals
194
195
  try {
195
- return { keyName, val: JSON.parse(value) }
196
+ val = value === 'undefined' ? undefined : JSON.parse(value)
197
+ return { keyName, val }
196
198
  } catch (e) {
197
199
  // plain string
198
200
  }
199
- return { keyName, val: _parsePrimitiveValue(edmRef, value) }
201
+ val = _parsePrimitiveValue(edmRef, value) //> REVISIT: undefined handling needed here as well?
202
+ return { keyName, val }
200
203
  }
201
- return { keyName, val: _parsePrimitiveValue(edmRef, segmentParam.getText()) }
204
+ val = segmentParam.getText() === undefined ? undefined : _parsePrimitiveValue(edmRef, segmentParam.getText())
205
+ return { keyName, val }
202
206
  }
203
207
 
204
208
  module.exports = {
@@ -17,6 +17,7 @@ const FullQualifiedName = require('../FullQualifiedName')
17
17
  const UriResource = require('./UriResource')
18
18
  const TransientStructuredType = require('../edm/TransientStructuredType')
19
19
  const FeatureSupport = require('../FeatureSupport')
20
+ const cds = require('../../../../../../cds')
20
21
 
21
22
  const TOKEN = "(?:[-!#$%&'*+.^_`|~A-Za-z0-9]+)"
22
23
  const FORMAT_REGEXP = new RegExp(
@@ -176,7 +177,7 @@ class UriParser {
176
177
  */
177
178
  parseQueryOptions (queryOptions, uriInfo) {
178
179
  // EXPERIMENTAL FEATURE FLAGS!
179
- const { okra_skip_query_options, odata_new_parser } = global.cds.env.features
180
+ const { okra_skip_query_options, odata_new_parser } = cds.env.features
180
181
  if (okra_skip_query_options && odata_new_parser) return
181
182
 
182
183
  const lastSegment = uriInfo.getLastSegment()
@@ -1,4 +1,7 @@
1
1
  'use strict'
2
+ const cds = require('../../../../../cds')
3
+ const TRACE = cds.debug('trace')
4
+ TRACE?.time('require okra'.padEnd(22))
2
5
 
3
6
  const commons = require('../odata-commons')
4
7
 
@@ -30,3 +33,5 @@ module.exports = {
30
33
  Components: require('./core/ComponentManager').Components, // eslint-disable-line global-require
31
34
  BatchExitHandler: require('./batch/BatchExitHandler') // eslint-disable-line global-require
32
35
  }
36
+
37
+ TRACE?.timeEnd('require okra'.padEnd(22))
@@ -1,3 +1,5 @@
1
+ const cds = require('../../../../cds')
2
+
1
3
  const {
2
4
  Components: { DATA_DELETE_HANDLER, DATA_READ_HANDLER, DATA_CREATE_HANDLER, DATA_UPDATE_HANDLER }
3
5
  } = require('../okra/odata-server')
@@ -158,54 +160,84 @@ const _addForeignKeys = (service, odataReq, data) => {
158
160
  }
159
161
  }
160
162
 
161
- const _getFunctionParameters = (lastSegment, keyValues, service, target) => {
163
+ const _getFunctionParameters = (lastSegment, keyValues, service, target, odataReq) => {
164
+ const { cds_assert: CDS_ASSERT } = cds.env.features
165
+
162
166
  const functionParameters = lastSegment.getFunctionParameters()
163
167
  const paramValues = _getParamData(functionParameters)
164
168
 
165
169
  // Working assumption for the case of name collisions: take the entity's key
166
- for (const key in keyValues) {
167
- paramValues[key] = keyValues[key]
168
- }
170
+ for (const key in keyValues) paramValues[key] = keyValues[key]
171
+
172
+ let assertTarget
173
+
169
174
  const errors = []
170
175
  if (lastSegment.getKind() === 'BOUND.FUNCTION') {
171
176
  const targetFunction = target && target.actions && target.actions[lastSegment.getFunction().getName()]
172
177
  if (!targetFunction.params) return {}
173
- convertStructured(service, targetFunction, paramValues, { errors })
178
+ if (!CDS_ASSERT) convertStructured(service, targetFunction, paramValues, { errors })
179
+ else assertTarget = targetFunction
174
180
  } else if (lastSegment.getKind() === 'FUNCTION.IMPORT') {
175
181
  const { namespace, name } = lastSegment.getFunctionImport().getFullQualifiedName()
176
182
  const targetFunction = service.model && service.model.definitions[`${namespace}.${name}`]
177
183
  if (!targetFunction.params) return {}
178
- convertStructured(service, targetFunction, paramValues, { errors })
184
+ if (!CDS_ASSERT) convertStructured(service, targetFunction, paramValues, { errors })
185
+ else assertTarget = targetFunction
179
186
  }
180
187
 
181
188
  if (errors.length > 1) throw Object.assign(new Error(MULTIPLE_ERRORS), { details: errors })
182
189
  if (errors.length === 1) throw errors[0]
183
190
 
191
+ if (CDS_ASSERT) {
192
+ const assertOptions = { filter: true, http: { req: odataReq.getIncomingRequest() } }
193
+ const errs = cds.assert(paramValues, assertTarget, assertOptions)
194
+ if (errs) {
195
+ if (errs.length === 1) throw errs[0]
196
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
197
+ }
198
+ }
199
+
184
200
  return paramValues
185
201
  }
186
202
 
187
- const _getCopiedData = (odataReq, lastSegment, service, target) => {
203
+ const _getCopiedData = (odataReq, segments, service, target) => {
204
+ const { cds_assert: CDS_ASSERT } = cds.env.features
205
+
206
+ const lastSegment = segments[segments.length - 1]
207
+
188
208
  let data = odataReq.getBody() || {}
209
+ let assertTarget = target
189
210
 
190
211
  if (lastSegment.getKind() === 'PRIMITIVE.PROPERTY') {
191
- return { [lastSegment.getProperty().getName()]: data }
192
- }
193
-
194
- data = deepCopy(data)
195
-
196
- if (lastSegment.getKind() === 'BOUND.ACTION') {
197
- const targetAction = target.actions && target.actions[lastSegment.getAction().getName()]
198
- if (!targetAction || !targetAction.params) return {}
199
- convertStructured(service, targetAction, data)
200
- } else if (lastSegment.getKind() === 'ACTION.IMPORT') {
201
- const { namespace, name } = lastSegment.getActionImport().getFullQualifiedName()
202
- const targetAction = service.model && service.model.definitions[`${namespace}.${name}`]
203
- if (!targetAction || !targetAction.params) return {}
204
- convertStructured(service, targetAction, data)
212
+ data = { [lastSegment.getProperty().getName()]: data }
213
+ if (cds.env.effective.odata.structs) {
214
+ for (let i = segments.length - 2; i >= 0; i--) {
215
+ const segment = segments[i]
216
+ if (segment.getKind() === 'COMPLEX.PROPERTY') {
217
+ const name = segment.getProperty().getName()
218
+ data = { [name]: data }
219
+ } else break
220
+ }
221
+ }
205
222
  } else {
206
- convertStructured(service, target, data)
223
+ data = deepCopy(data)
224
+ if (lastSegment.getKind() === 'BOUND.ACTION') {
225
+ const targetAction = target.actions && target.actions[lastSegment.getAction().getName()]
226
+ if (!targetAction || !targetAction.params) return { data: {} }
227
+ if (!CDS_ASSERT) convertStructured(service, targetAction, data)
228
+ else assertTarget = targetAction
229
+ } else if (lastSegment.getKind() === 'ACTION.IMPORT') {
230
+ const { namespace, name } = lastSegment.getActionImport().getFullQualifiedName()
231
+ const targetAction = service.model && service.model.definitions[`${namespace}.${name}`]
232
+ if (!targetAction || !targetAction.params) return { data: {} }
233
+ if (!CDS_ASSERT) convertStructured(service, targetAction, data)
234
+ else assertTarget = targetAction
235
+ } else {
236
+ if (!CDS_ASSERT) convertStructured(service, target, data)
237
+ }
207
238
  }
208
- return data
239
+
240
+ return { data, assertTarget }
209
241
  }
210
242
 
211
243
  /**
@@ -231,7 +263,7 @@ const getData = (component, odataReq, service, target) => {
231
263
  const keyValues = _getParamData(keyPredicates)
232
264
 
233
265
  if (component === DATA_READ_HANDLER && _isFunctionInvocation(odataReq)) {
234
- return _getFunctionParameters(lastSegment, keyValues, service, target)
266
+ return _getFunctionParameters(lastSegment, keyValues, service, target, odataReq)
235
267
  }
236
268
 
237
269
  if (component === DATA_DELETE_HANDLER || component === DATA_READ_HANDLER) {
@@ -242,7 +274,7 @@ const getData = (component, odataReq, service, target) => {
242
274
  }
243
275
 
244
276
  // copy so that original payload is preserved
245
- const data = _getCopiedData(odataReq, lastSegment, service, target)
277
+ const { data, assertTarget } = _getCopiedData(odataReq, segments, service, target)
246
278
 
247
279
  // Only to be done for post via navigation
248
280
  if (
@@ -263,6 +295,20 @@ const getData = (component, odataReq, service, target) => {
263
295
  Array.isArray(data) ? Object.assign(data[0], keyValues) : Object.assign(data, keyValues)
264
296
  }
265
297
 
298
+ // we need the (foreign) keys from the URL before asserting -> we can't assert in _getCopiedData()
299
+ if (cds.env.features.cds_assert && assertTarget) {
300
+ const assertOptions = {
301
+ filter: true,
302
+ http: { req: odataReq.getIncomingRequest() },
303
+ mandatories: component === 'CREATE' || undefined
304
+ }
305
+ const errs = cds.assert(data, assertTarget, assertOptions)
306
+ if (errs) {
307
+ if (errs.length === 1) throw errs[0]
308
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
309
+ }
310
+ }
311
+
266
312
  return data
267
313
  }
268
314
 
@@ -255,11 +255,15 @@ const _isSingleEntity = options => {
255
255
  return isServiceEntity || isTargetComposition
256
256
  }
257
257
 
258
+ const _isStructuredProperty = ({ returnType }) => {
259
+ return returnType.elements && returnType.kind === 'element'
260
+ }
261
+
258
262
  const _getContextUrl = options => {
259
263
  if (!options.returnType) return ''
260
264
  const contextUrlPrefix = _getContextUrlPrefix(options)
261
265
  const returnTypeUrl = _getReturnTypeUrl(options)
262
- const columnsStringified = _listColumns(options)
266
+ const columnsStringified = options.propertyName || _isStructuredProperty(options) ? '' : _listColumns(options)
263
267
  const $entity = _isSingleEntity(options) ? '/$entity' : ''
264
268
  return `${contextUrlPrefix}$metadata#${returnTypeUrl}${columnsStringified}${$entity}`
265
269
  }
@@ -299,6 +303,7 @@ const _partialCopyColumns = query => {
299
303
  return []
300
304
  }
301
305
 
306
+ // eslint-disable-next-line complexity
302
307
  const _getPathInfo = (query, model) => {
303
308
  const queryFrom =
304
309
  (query.SELECT && resolveFromSelect(query)) ||
@@ -321,7 +326,10 @@ const _getPathInfo = (query, model) => {
321
326
  returnType = target
322
327
  isCollection = Array.isArray(query) || (query.SELECT && !query.SELECT.one)
323
328
  propertyName = query._propertyAccess && query.SELECT.columns[0].ref[query.SELECT.columns[0].ref.length - 1]
324
- if (propertyName) returnType = query.SELECT.columns[0].ref.reduce((r, c) => r.elements[c], target)
329
+ if (propertyName && path.slice(-1)[0] !== propertyName) {
330
+ path.push(propertyName)
331
+ returnType = query.SELECT.columns[0].ref.reduce((r, c) => r.elements[c], target)
332
+ }
325
333
  }
326
334
  const isStream = propertyName && target.elements[propertyName]?.['@Core.MediaType']
327
335
  return {
@@ -14,7 +14,12 @@ const setLocationHeader = (req, { model }) => {
14
14
  const { odataRes } = req._
15
15
  const cqn = getSimpleSelectCQN(req.target, req.data)
16
16
  const { path: location } = cds.odata.urlify(cqn, { kind: 'odata', model, method: 'GET' })
17
- odataRes.setHeader('Location', location.replace(/\?.*$/, ''))
17
+ // REVISIT: needs reworking for new adapter, especially re $batch
18
+ if (odataRes) {
19
+ odataRes.setHeader('Location', location.replace(/\?.*$/, ''))
20
+ } else if (req.http?.res) {
21
+ req.http.res.set('location', location.replace(/\?.*$/, ''))
22
+ }
18
23
  }
19
24
 
20
25
  const _isNoAccessError = e => Number(e.code) === 403 || Number(e.code) === 401