@sap/cds 7.8.1 → 7.9.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 (137) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/_i18n/i18n_ar.properties +3 -0
  3. package/_i18n/i18n_cs.properties +3 -0
  4. package/_i18n/i18n_da.properties +3 -0
  5. package/_i18n/i18n_es_MX.properties +3 -0
  6. package/_i18n/i18n_fi.properties +3 -0
  7. package/_i18n/i18n_hu.properties +6 -0
  8. package/_i18n/i18n_ko.properties +3 -0
  9. package/_i18n/i18n_ms.properties +3 -0
  10. package/_i18n/i18n_nl.properties +3 -0
  11. package/_i18n/i18n_no.properties +3 -0
  12. package/_i18n/i18n_ro.properties +3 -0
  13. package/_i18n/i18n_sv.properties +3 -0
  14. package/_i18n/i18n_th.properties +3 -0
  15. package/_i18n/i18n_tr.properties +6 -0
  16. package/_i18n/i18n_zh_TW.properties +3 -0
  17. package/bin/serve.js +5 -5
  18. package/lib/auth/basic-auth.js +1 -1
  19. package/lib/compile/cdsc.js +33 -6
  20. package/lib/compile/etc/_localized.js +14 -7
  21. package/lib/compile/for/lean_drafts.js +9 -0
  22. package/lib/compile/to/edm-files.js +116 -0
  23. package/lib/compile/to/edm.js +8 -1
  24. package/lib/compile/to/hdbtabledata.js +3 -3
  25. package/lib/compile/to/sql.js +4 -2
  26. package/lib/compile/to/yaml.js +22 -21
  27. package/lib/dbs/cds-deploy.js +5 -6
  28. package/lib/env/cds-env.js +7 -0
  29. package/lib/env/cds-requires.js +20 -1
  30. package/lib/env/defaults.js +21 -5
  31. package/lib/env/schemas/cds-package.js +1 -1
  32. package/lib/env/schemas/cds-rc.js +85 -4
  33. package/lib/index.js +1 -1
  34. package/lib/linked/classes.js +2 -2
  35. package/lib/linked/entities.js +10 -0
  36. package/lib/linked/models.js +1 -1
  37. package/lib/plugins.js +1 -1
  38. package/lib/ql/INSERT.js +17 -3
  39. package/lib/ql/Query.js +4 -0
  40. package/lib/ql/infer.js +1 -1
  41. package/lib/req/request.js +1 -1
  42. package/lib/srv/cds-serve.js +1 -0
  43. package/lib/srv/middlewares/cds-context.js +1 -1
  44. package/lib/srv/protocols/odata-v4.js +5 -6
  45. package/lib/srv/srv-models.js +9 -2
  46. package/lib/utils/cds-test.js +2 -0
  47. package/lib/utils/cds-utils.js +9 -4
  48. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
  53. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
  64. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
  65. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
  66. package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
  67. package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
  68. package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
  69. package/libx/_runtime/common/generic/auth/index.js +2 -0
  70. package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
  71. package/libx/_runtime/common/generic/auth/restrict.js +6 -5
  72. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  73. package/libx/_runtime/common/generic/crud.js +5 -8
  74. package/libx/_runtime/common/generic/etag.js +8 -6
  75. package/libx/_runtime/common/generic/sorting.js +2 -2
  76. package/libx/_runtime/common/i18n/messages.properties +1 -0
  77. package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
  78. package/libx/_runtime/common/utils/compareJson.js +274 -0
  79. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  80. package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
  81. package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
  82. package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
  83. package/libx/_runtime/common/utils/resolveView.js +0 -16
  84. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  85. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  86. package/libx/_runtime/common/utils/streamProp.js +9 -2
  87. package/libx/_runtime/common/utils/ucsn.js +1 -1
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  89. package/libx/_runtime/db/generic/rewrite.js +7 -13
  90. package/libx/_runtime/fiori/generic/activate.js +1 -1
  91. package/libx/_runtime/fiori/generic/edit.js +1 -1
  92. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  93. package/libx/_runtime/fiori/lean-draft.js +151 -46
  94. package/libx/_runtime/fiori/utils/handler.js +1 -1
  95. package/libx/_runtime/hana/execute.js +6 -2
  96. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  97. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  98. package/libx/_runtime/messaging/event-broker.js +212 -0
  99. package/libx/_runtime/remote/Service.js +9 -32
  100. package/libx/_runtime/remote/utils/client.js +13 -21
  101. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
  102. package/libx/_runtime/sqlite/execute.js +8 -3
  103. package/libx/_runtime/ucl/Service.js +259 -0
  104. package/libx/common/assert/index.js +6 -11
  105. package/libx/common/assert/validation.js +6 -1
  106. package/libx/odata/index.js +47 -25
  107. package/libx/odata/middleware/batch.js +8 -7
  108. package/libx/odata/middleware/create.js +42 -16
  109. package/libx/odata/middleware/delete.js +18 -11
  110. package/libx/odata/middleware/metadata.js +15 -14
  111. package/libx/odata/middleware/operation.js +30 -40
  112. package/libx/odata/middleware/parse.js +2 -3
  113. package/libx/odata/middleware/read.js +59 -52
  114. package/libx/odata/middleware/service-document.js +7 -7
  115. package/libx/odata/middleware/stream.js +26 -24
  116. package/libx/odata/middleware/update.js +53 -92
  117. package/libx/odata/parse/afterburner.js +45 -47
  118. package/libx/odata/parse/grammar.peggy +3 -3
  119. package/libx/odata/parse/multipartToJson.js +10 -22
  120. package/libx/odata/parse/parser.js +1 -1
  121. package/libx/odata/utils/etag.js +13 -0
  122. package/libx/odata/utils/handler.js +120 -0
  123. package/libx/odata/utils/index.js +15 -2
  124. package/libx/odata/utils/metaInfo.js +410 -0
  125. package/libx/odata/utils/path.js +5 -2
  126. package/libx/odata/utils/readAfterWrite.js +23 -0
  127. package/libx/odata/utils/result.js +4 -5
  128. package/libx/rest/RestAdapter.js +4 -13
  129. package/libx/rest/middleware/parse.js +40 -7
  130. package/package.json +1 -1
  131. package/server.js +2 -1
  132. package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
  133. package/libx/_runtime/common/utils/thenable.js +0 -51
  134. package/libx/_runtime/rest/service.js +0 -2
  135. package/libx/odata/parse/parseToCqn.js +0 -39
  136. package/libx/rest/middleware/input.js +0 -54
  137. package/libx/rest/middleware/payload.js +0 -13
@@ -3,7 +3,7 @@ const { Readable } = require('node:stream')
3
3
  const getError = require('../../_runtime/common/error')
4
4
  const { getTransition } = require('../../_runtime/common/utils/resolveView')
5
5
  const LOG = cds.log('odata')
6
- const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
6
+ const { getKeysAndParamsFromPath, handleSapMessages, validateIfNoneMatch } = require('../utils')
7
7
 
8
8
  const _resolveContentProperty = (target, annotName, resolvedProp) => {
9
9
  if (target.elements[resolvedProp]) {
@@ -20,8 +20,10 @@ const _resolveContentProperty = (target, annotName, resolvedProp) => {
20
20
 
21
21
  const isStream = query => {
22
22
  const { _propertyAccess, target } = query
23
+ if (!_propertyAccess) return
24
+
23
25
  const element = target.elements[_propertyAccess]
24
- return Boolean(element?.['@Core.MediaType'])
26
+ return element._type === 'cds.LargeBinary' && element['@Core.MediaType']
25
27
  }
26
28
 
27
29
  const isStreamByDollarValue = (query, previous, last) => {
@@ -47,7 +49,7 @@ const _addMetadataProperty = (query, property, annotName, odataName) => {
47
49
  const addStreamMetadata = query => {
48
50
  // new odata parser sets streaming property in SELECT.from
49
51
  const ref = query.SELECT.columns?.[0].ref || query.SELECT.from.ref
50
- const propertyName = ref[ref.length - 1]
52
+ const propertyName = ref.at(-1)
51
53
  let mediaTypeProperty
52
54
  for (let key in query.target.elements) {
53
55
  const val = query.target.elements[key]
@@ -76,19 +78,16 @@ const addStreamMetadata = query => {
76
78
  }
77
79
  }
78
80
 
79
- const validateStream = (req, res, result) => {
81
+ const validateStream = (req, result) => {
80
82
  // REVISIT: compat, should actually be treated as object
81
83
  if (!Array.isArray(result)) result = [result]
82
84
 
83
85
  // Reading one entity or a property of it should yield only a result length of one.
84
- if (result.length === 0 || result[0] === undefined) {
85
- if (req.headers['if-none-match']) return
86
- throw getError(404)
87
- }
86
+ if (result.length === 0 || result[0] === undefined) throw getError(404)
88
87
 
89
88
  if (result.length > 1) throw getError(400)
90
89
 
91
- if (result[0] === null) return null
90
+ if (result[0] === null) return
92
91
 
93
92
  result = result[0]
94
93
 
@@ -102,9 +101,8 @@ const validateStream = (req, res, result) => {
102
101
  !headers.accept.includes(contentType) &&
103
102
  !headers.accept.includes(contentType.split('/')[0] + '/*')
104
103
  ) {
105
- throw Object.assign(new Error(`Content type "${contentType}" not listed in accept header "${headers.accept}".`), {
106
- statusCode: 406
107
- })
104
+ const msg = `Content type "${contentType}" is not listed in accept header "${headers.accept}"`
105
+ throw Object.assign(new Error(msg), { statusCode: 406 })
108
106
  }
109
107
  }
110
108
 
@@ -173,7 +171,7 @@ const stream = srv =>
173
171
  const { _query: query } = req
174
172
 
175
173
  // $apply with concat -> multiple queries with special handling -> read only, no stream?
176
- if (Array.isArray(query)) return next(null, req, res)
174
+ if (Array.isArray(query)) return next()
177
175
 
178
176
  const [previous, lastPathElement] = req.path.split('/').slice(-2)
179
177
  const _isStreamByDollarValue = isStreamByDollarValue(query, previous, lastPathElement)
@@ -188,16 +186,16 @@ const stream = srv =>
188
186
  }
189
187
  }
190
188
 
191
- query.SELECT.columns ??= ['*']
189
+ const pdfMimeType = !!req.headers.accept?.match(/application\/pdf/)
190
+ const isMimeTypeStreamedByDefault = !!(!query.SELECT.one && pdfMimeType)
191
+ const _isStream = isStream(query) || _isStreamByDollarValue || isMimeTypeStreamedByDefault
192
192
 
193
- const _isStream = isStream(query) || _isStreamByDollarValue
193
+ if (!_isStream) return next()
194
194
 
195
- if (!_isStream) {
196
- return next(null, req, res)
195
+ if (!query.target['@cds.persistence.skip'] && !isMimeTypeStreamedByDefault) {
196
+ addStreamMetadata(query)
197
197
  }
198
198
 
199
- if (!query.target['@cds.persistence.skip']) addStreamMetadata(query)
200
-
201
199
  // we need the cds request, so we can access the modified query, which is cloned due to lean-draft, so we need to use dispatch here and pass a cds req
202
200
  const cdsReq = new cds.Request({ query, req, res })
203
201
  Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
@@ -214,18 +212,22 @@ const stream = srv =>
214
212
  .tx(() => {
215
213
  return srv.dispatch(cdsReq).then(async result => {
216
214
  handleSapMessages(cdsReq, req, res)
217
- validateStream(req, res, result)
215
+ validateStream(req, result)
216
+
217
+ if (validateIfNoneMatch(cdsReq, req, result)) {
218
+ return res.status(304).end()
219
+ }
218
220
 
219
221
  const stream = normalizeStream(result, query._propertyAccess, lastPathElement, query.target)
220
222
  if (stream === null) {
221
- if (req.headers['if-none-match']) {
222
- res.status(304)
223
- return
224
- }
225
223
  res.status(204)
226
224
  return
227
225
  }
228
226
 
227
+ if (pdfMimeType) {
228
+ if (!result.$mediaContentType) result.$mediaContentType = 'application/pdf'
229
+ }
230
+
229
231
  setStreamingHeaders(result, res)
230
232
 
231
233
  return new Promise((resolve, reject) => {
@@ -1,13 +1,12 @@
1
1
  const cds = require('../../../')
2
- const { INSERT, UPDATE } = cds.ql
2
+ const { UPDATE } = cds.ql
3
3
 
4
4
  const { toODataResult, postProcess } = require('../utils/result')
5
- const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
5
+ const { getKeysAndParamsFromPath, handleSapMessages, getPreferReturnHeader } = require('../utils')
6
6
  const { deepCopy } = require('../../_runtime/common/utils/copy')
7
7
 
8
- // REVISIT: move to or rewrite in libx/odata
9
- const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
10
- const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
8
+ const readAfterWrite = require('../utils/readAfterWrite')
9
+ const metaInfo = require('../utils/metaInfo')
11
10
 
12
11
  const _isUpsertAllowed = ({ target, data, event }) => {
13
12
  return (
@@ -42,7 +41,7 @@ const _isNavigationWithKeyInParent = (keys, data, pathExpression, model) => {
42
41
  return parent && navElement && where
43
42
  }
44
43
 
45
- module.exports = srv =>
44
+ module.exports = (srv, router) =>
46
45
  function update(req, res, next) {
47
46
  // REVISIT: better solution for _propertyAccess
48
47
  const {
@@ -53,16 +52,11 @@ module.exports = srv =>
53
52
 
54
53
  // REVISIT: patch on collection is allowed in odata 4.01
55
54
  if (!one) {
56
- // REVISIT: don't use "ENTITY.COLLECTION" as that's an okra term
57
- throw Object.assign(new Error(`Method ${req.method} not allowed for ENTITY.COLLECTION`), {
58
- statusCode: 405
59
- })
55
+ throw Object.assign(new Error(`Method ${req.method} is not allowed for entity collections`), { statusCode: 405 })
60
56
  }
61
57
 
62
58
  if (_propertyAccess && req.method === 'PATCH') {
63
- throw Object.assign(new Error(`Method ${req.method} not allowed for PRIMITIVE.PROPERTY`), {
64
- statusCode: 405
65
- })
59
+ throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
66
60
  }
67
61
 
68
62
  // payload & params
@@ -87,11 +81,16 @@ module.exports = srv =>
87
81
  // query
88
82
  let query = UPDATE.entity(from).with(data)
89
83
 
84
+ // cdsReq.headers should contain merged headers of envelope and subreq
85
+ const headers = { ...cds.context.http.req.headers, ...req.headers }
86
+
90
87
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
91
- let cdsReq = new cds.Request({ query, params, req, res })
88
+ let cdsReq = new cds.Request({ query, params, headers, req, res })
92
89
  Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
93
90
 
94
- let crudEvent = 'UPDATE'
91
+ // API for subrequests of $batch (or incoming request)
92
+ cdsReq.req = req
93
+ cdsReq.res = res
95
94
 
96
95
  // REVISIT: adjust in getter?
97
96
  if (req.method === 'PUT') cdsReq.method = 'PUT'
@@ -104,98 +103,60 @@ module.exports = srv =>
104
103
  // or the auto-managed tx opened for the respective atomicity group, if exists
105
104
  return srv
106
105
  .run(() => {
107
- return srv
108
- .dispatch(cdsReq)
109
- .catch(async e => {
110
- // if no UPSERT is allowed, continue with error
111
- const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
112
-
113
- const isForcedInsert =
114
- (e.code === 412 || e.status === 412 || e.statusCode === 412) && req.headers['if-none-match'] === '*'
115
-
116
- if (
117
- _propertyAccess ||
118
- !((is404 || isForcedInsert) && _isUpsertAllowed({ target, data, event: req.method }))
119
- ) {
120
- throw e
121
- }
122
-
123
- // PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
124
- if (req.headers['if-match']) throw Object.assign(new Error('412'), { statusCode: 412 })
125
-
126
- // check only works with req.body and not with updateDate
127
- if (_isNavigationWithKeyInParent(target.keys, req.body, from, srv.model)) {
128
- // REVISIT: better error message
129
- throw Object.assign(new Error('Unprocessable Content'), { statusCode: 422 })
130
- }
131
-
132
- // REVISIT:
133
- // can we somehow "replay" the request with POST?
134
- // or should we call the create handler directly?
135
-
136
- // payload & params
137
- data = deepCopy(req.body)
138
- // add keys from url into payload (overwriting if already present)
139
- Object.assign(data, keys)
140
-
141
- // assert payload
142
- const assertOptions = { filter: true, http: { req }, mandatories: true }
143
- const errs = cds.assert(data, target, assertOptions)
144
- if (errs) {
145
- if (errs.length === 1) throw errs[0]
146
- throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
147
- }
148
-
149
- crudEvent = 'CREATE'
150
-
151
- // query
152
- // REVISIT: up_XX needs to be looked up -> composition of aspect
153
- query = INSERT.into(from).entries(data)
154
-
155
- // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
156
- cdsReq = new cds.Request({ query: query, params, req, res })
157
-
158
- return srv.dispatch(cdsReq)
159
- })
160
- .then(result => {
161
- // REVISIT: not great, but avoids try catch in catch callback above
162
- if (result.constructor.name === 'ServerResponse') return
163
- handleSapMessages(cdsReq, req, res)
164
-
165
- // TODO: any other checks needed?
166
- if (cdsReq._.readAfterWrite && !(_propertyAccess && !target._etag))
167
- return readAfterWrite(cdsReq, srv, { operation: { result } })
168
-
169
- return result
170
- })
106
+ return srv.dispatch(cdsReq).then(result => {
107
+ handleSapMessages(cdsReq, req, res)
108
+
109
+ // TODO: any other checks needed?
110
+ if (cdsReq._.readAfterWrite && !(_propertyAccess && !target._etag)) {
111
+ return readAfterWrite(cdsReq, srv, SELECT.one(cdsReq.subject))
112
+ }
113
+
114
+ return result
115
+ })
171
116
  })
172
117
  .then(result => {
173
118
  // we use an extra then block, after getting the result, so the transaction is commited, before sending the response
174
119
 
120
+ if (result == null) return res.sendStatus(204)
121
+
175
122
  // REVISIT: metaInfo needs original query in case of property access, but why?
176
- const info = metaInfo(_propertyAccess ? req._query : query, crudEvent, srv, result, req)
123
+ const info = metaInfo(_propertyAccess ? req._query : query, 'UPDATE', srv, result, req)
177
124
 
178
- if (result == null) return res.sendStatus(204)
125
+ const isMinimal = getPreferReturnHeader(req) === 'minimal'
179
126
 
180
- const isMinimal = req._preferReturn === 'minimal'
181
127
  postProcess(cdsReq.target, srv, result, isMinimal)
182
- if (result['$etag']) res.set('etag', result['$etag'])
183
128
 
184
- if (crudEvent === 'CREATE') {
185
- // UPSERT
186
- return res
187
- .set('Content-Type', 'application/json;IEEE754Compatible=true')
188
- .status(201)
189
- .send(toODataResult(result, info))
190
- }
129
+ if (result['$etag']) res.set('etag', result['$etag'])
191
130
 
192
131
  if (isMinimal || (query._propertyAccess && result[query._propertyAccess] == null) || info.metadata.isStream) {
193
132
  return res.sendStatus(204)
194
133
  }
195
134
 
196
135
  result = toODataResult(result, info)
136
+ res.set('content-type', 'application/json;IEEE754Compatible=true')
137
+ res.send(result)
138
+ })
139
+ .catch(e => {
140
+ // if UPSERT is allowed, redirect to POST
141
+ const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
142
+ const isForcedInsert =
143
+ (e.code === 412 || e.status === 412 || e.statusCode === 412) && req.headers['if-none-match'] === '*'
144
+ if (!_propertyAccess && (is404 || isForcedInsert) && _isUpsertAllowed({ target, data, event: req.method })) {
145
+ // PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
146
+ if (req.headers['if-match']) {
147
+ return next(Object.assign(new Error('412'), { statusCode: 412 }))
148
+ }
149
+ // (check only works with req.body and not with data)
150
+ if (_isNavigationWithKeyInParent(target.keys, req.body, from, srv.model)) {
151
+ return next(Object.assign(new Error('422'), { statusCode: 422 }))
152
+ }
153
+ // -> redirect to POST
154
+ return router.handle(Object.assign(Object.create(req), { method: 'POST' }), res)
155
+ }
156
+
157
+ handleSapMessages(cdsReq, req, res)
197
158
 
198
- return res.set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
159
+ // continue with caught error
160
+ next(e)
199
161
  })
200
- .catch(next)
201
162
  }
@@ -178,14 +178,20 @@ function _convertVal(value, element) {
178
178
  case 'cds.Integer':
179
179
  case 'cds.Int16':
180
180
  case 'cds.Int32':
181
- if (!/^-?\+?\d+$/.test(value))
182
- throw Object.assign(new Error(`${element.name} does not contain a valid Integer`), { statusCode: 400 })
181
+ if (!/^-?\+?\d+$/.test(value)) {
182
+ const msg = `Element "${element.name}" does not contain a valid Integer`
183
+ throw Object.assign(new Error(msg), { statusCode: 400 })
184
+ }
183
185
  // eslint-disable-next-line no-case-declarations
184
186
  const n = Number(value)
185
- if (!Number.isSafeInteger(n))
186
- throw Object.assign(new Error(`${element.name} does not contain a valid Integer`), { statusCode: 400 })
187
- if (element._type === 'cds.UInt8' && n < 0)
188
- throw Object.assign(new Error(`${element.name} does not contain a valid positive Integer`), { statusCode: 400 })
187
+ if (!Number.isSafeInteger(n)) {
188
+ const msg = `Element "${element.name}" does not contain a valid Integer`
189
+ throw Object.assign(new Error(msg), { statusCode: 400 })
190
+ }
191
+ if (element._type === 'cds.UInt8' && n < 0) {
192
+ const msg = `Element "${element.name}" does not contain a valid positive Integer`
193
+ throw Object.assign(new Error(msg), { statusCode: 400 })
194
+ }
189
195
  return n
190
196
  case 'cds.Double':
191
197
  return parseFloat(value)
@@ -260,7 +266,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
260
266
  ) {
261
267
  incompleteKeys = false
262
268
  } else {
263
- const msg = `"${action.name}" must be called on a single instance of "${current.name}".`
269
+ const msg = `${action.kind[0].toUpperCase() + action.kind.slice(1)} "${action.name}" must be called on a single instance of ${current.name}`
264
270
  throw Object.assign(new Error(msg), { statusCode: 400 })
265
271
  }
266
272
  }
@@ -332,10 +338,8 @@ function _processSegments(from, model, namespace, cqn, protocol) {
332
338
  ref[i].where = undefined
333
339
  if (ref[i + 1] !== 'Set') {
334
340
  // /Set is missing
335
- throw cds.error(`Invalid call to "${current.name}". You need to navigate to Set`, {
336
- code: '400',
337
- statusCode: 400
338
- })
341
+ const msg = `Invalid call to "${current.name}". You need to navigate to Set`
342
+ throw cds.error(msg, { code: '400', statusCode: 400 })
339
343
  }
340
344
  ref[++i] = null
341
345
  } else if (current.kind === 'entity') {
@@ -356,18 +360,25 @@ function _processSegments(from, model, namespace, cqn, protocol) {
356
360
  } else if ({ action: 1, function: 1 }[current.kind]) {
357
361
  // > action or function
358
362
  if (current.kind === 'action' && ref && ref[ref.length - 1]?.where?.length === 0) {
359
- const msg = `Round brackets (parentheses) are not allowed for action calls.`
363
+ const msg = `Parentheses are not allowed for action calls.`
360
364
  throw Object.assign(new Error(msg), { statusCode: 400 })
361
365
  }
362
366
 
363
367
  if (i !== ref.length - 1) {
364
- const msg = `${i ? 'Unbound' : 'Bound'} ${current.kind} are only supported as the last path segment.`
368
+ const msg = `${i ? 'Unbound' : 'Bound'} ${current.kind}s are only supported as the last path segment`
365
369
  throw Object.assign(new Error(msg), { statusCode: 501 })
366
370
  }
367
371
  ref[i] = { operation: current.name }
368
372
  if (params) ref[i].args = params
369
373
  if (current.returns && current.returns._type) one = true
370
374
  } else if (current.isAssociation) {
375
+ if (!current._target._service) {
376
+ // not exposed target
377
+ cds.error(`Property '${current.name}' does not exist in type '${target.name.replace(namespace + '.', '')}'`, {
378
+ statusCode: 404
379
+ })
380
+ }
381
+
371
382
  // > navigation
372
383
  one = !!(current.is2one || ref[i].where)
373
384
  incompleteKeys = one || i === ref.length - 1 ? false : true
@@ -425,14 +436,8 @@ function _processSegments(from, model, namespace, cqn, protocol) {
425
436
 
426
437
  if (incompleteKeys) {
427
438
  // > last segment not fully qualified
428
- throw Object.assign(
429
- new Error(
430
- `Entity "${current.name}" has ${_keysOf(current).length} keys. Only ${keyCount} ${
431
- keyCount === 1 ? 'was' : 'were'
432
- } provided.`
433
- ),
434
- { statusCode: 400 }
435
- )
439
+ const msg = `Entity "${current.name}" has ${_keysOf(current).length} keys. Only ${keyCount} ${keyCount === 1 ? 'was' : 'were'} provided.`
440
+ throw Object.assign(new Error(msg), { statusCode: 400 })
436
441
  }
437
442
 
438
443
  // remove all nulled refs
@@ -441,7 +446,8 @@ function _processSegments(from, model, namespace, cqn, protocol) {
441
446
  return { one, current, target }
442
447
  }
443
448
 
444
- const AGGREGATION_DEFAULT = '@Aggregation.default'
449
+ const AGGR_DFLT = '@Aggregation.default'
450
+ const CSTM_AGGR = '@Aggregation.CustomAggregate'
445
451
 
446
452
  function _addKeys(columns, target) {
447
453
  let hasAggregatedColumn = false,
@@ -506,7 +512,7 @@ function _processColumns(cqn, target, protocol) {
506
512
 
507
513
  if (!Array.isArray(columns)) return
508
514
 
509
- let aggrProp, aggrElem
515
+ let aggrProp, aggrElem, defaultAggregation
510
516
  for (let i = 0; i < columns.length; i++) {
511
517
  if (
512
518
  columns[i].func === null &&
@@ -518,15 +524,14 @@ function _processColumns(cqn, target, protocol) {
518
524
  // REVISIT: also support aggregate(Sales/Amount)?
519
525
  aggrProp = columns[i].args[0].ref[0]
520
526
  aggrElem = target.elements[aggrProp]
521
- if (
522
- aggrElem &&
523
- target[`@Aggregation.CustomAggregate#${aggrProp}`] &&
524
- aggrElem[AGGREGATION_DEFAULT] &&
525
- aggrElem[AGGREGATION_DEFAULT]['#']
526
- ) {
527
- columns[i].func = aggrElem[AGGREGATION_DEFAULT]['#'].toLowerCase()
527
+ if (aggrElem && target[`${CSTM_AGGR}#${aggrProp}`] && aggrElem[AGGR_DFLT] && aggrElem[AGGR_DFLT]['#']) {
528
+ defaultAggregation = aggrElem[AGGR_DFLT]['#'].toLowerCase()
529
+ if (defaultAggregation === 'count_distinct') defaultAggregation = 'countdistinct'
530
+ columns[i].func = defaultAggregation
528
531
  columns[i].as = columns[i].as || aggrProp
529
- } else throw new Error(`Default aggregation for property '${aggrProp}' not found.`)
532
+ } else {
533
+ throw new Error(`Default aggregation for property "${aggrProp}" not found`)
534
+ }
530
535
  }
531
536
  }
532
537
  }
@@ -555,26 +560,18 @@ const _checkAllKeysProvided = (params, entity) => {
555
560
  continue
556
561
  }
557
562
 
558
- throw Object.assign(
559
- new Error(
560
- `${isView ? 'Parameter' : 'Key'} "${keyOfEntity}" is missing for ${isView ? 'view' : 'entity'} "${
561
- entity.name
562
- }"`
563
- ),
564
- { statusCode: 400 }
565
- )
563
+ // prettier-ignore
564
+ const msg = `${isView ? 'Parameter' : 'Key'} "${keyOfEntity}" is missing for ${isView ? 'view' : 'entity'} "${entity.name}"`
565
+ throw Object.assign(new Error(msg), { statusCode: 400 })
566
566
  }
567
567
  }
568
568
  }
569
569
 
570
570
  const _doesNotExistError = (isExpand, refName, targetName) => {
571
- if (isExpand) {
572
- throw Object.assign(new Error(`Navigation property '${refName}' is not defined in '${targetName}'`), {
573
- statusCode: 400
574
- })
575
- } else {
576
- throw Object.assign(new Error(`Property '${refName}' does not exist in '${targetName}'`), { statusCode: 400 })
577
- }
571
+ const msg = isExpand
572
+ ? `Navigation property "${refName}" is not defined in ${targetName}`
573
+ : `Property "${refName}" does not exist in ${targetName}`
574
+ throw Object.assign(new Error(msg), { statusCode: 400 })
578
575
  }
579
576
 
580
577
  function _validateXpr(xpr, ignoredColumns, target, isOne, model, aliases = []) {
@@ -706,8 +703,9 @@ function _4service(service) {
706
703
  namespace
707
704
  )
708
705
  // REVISIT: 404 or 400?
709
- if (!root)
706
+ if (!root) {
710
707
  cds.error(`Invalid resource path "${namespace}.${ref[0].id || ref[0]}"`, { code: '404', statusCode: 404 })
708
+ }
711
709
  if (ref[0].id) ref[0].id = root.name
712
710
  else ref[0] = root.name
713
711
 
@@ -714,7 +714,7 @@
714
714
  function
715
715
  = func:functionName OPEN args:functionArgs CLOSE {
716
716
  if (strict && !(func.toLowerCase() in strict.functions)) {
717
- throw Object.assign(new Error(`Function '${func}' is not supported`), { statusCode: 400 })
717
+ throw Object.assign(new Error(`Function "${func}" is not supported`), { statusCode: 501 })
718
718
  }
719
719
  return { func: func.toLowerCase(), args }
720
720
  }
@@ -723,7 +723,7 @@
723
723
  = args:(a:operand more:( COMMA o:operand { return o } )* { return [ a, ...more ] })* { return args.length ? args[0] : args }
724
724
 
725
725
  boolish
726
- = func:("contains"i/"endswith"i/"startswith"i) OPEN a:operand COMMA b:operand CLOSE
726
+ = func:("contains"i/"endswith"i/"startswith"i/"matchespattern"i) OPEN a:operand COMMA b:operand CLOSE
727
727
  { return { func: func.toLowerCase(), args:[a,b] }}
728
728
 
729
729
  NOT = o "NOT"i _ {return 'not'}
@@ -871,7 +871,7 @@
871
871
  ( "Z" / (("+" / "-")[0-9][0-9]":"[0-9][0-9]) )? // timezone (Z or +-hh:mm)
872
872
  )?) {
873
873
  if (s.split('-')[0].length > 4)
874
- throw Object.assign(new Error(`The type 'Edm.DateTimeOffset' is not compatible with '${s}'`), { statusCode: 400 })
874
+ throw Object.assign(new Error(`The type Edm.DateTimeOffset is not compatible with "${s}"`), { statusCode: 400 })
875
875
  return s
876
876
  }
877
877
 
@@ -10,6 +10,7 @@ const parseStream = async function* (body, boundary) {
10
10
 
11
11
  try {
12
12
  const boundaries = [boundary]
13
+ let content_id
13
14
  let yielded = 0
14
15
  const requests = []
15
16
  let idCount = 0
@@ -26,6 +27,7 @@ const parseStream = async function* (body, boundary) {
26
27
  }
27
28
  const newBoundary = req.headers['content-type']?.match(/boundary=(.*)/i)?.[1]
28
29
  if (newBoundary) boundaries.push(newBoundary)
30
+ content_id = req.headers['content-id']
29
31
  return
30
32
  }
31
33
 
@@ -54,6 +56,7 @@ const parseStream = async function* (body, boundary) {
54
56
  }
55
57
 
56
58
  if (boundaries.length > 1) request.atomicityGroup = boundaries.at(-1)
59
+ if (content_id) request.content_id = content_id
57
60
 
58
61
  requests.push(request)
59
62
  }
@@ -69,28 +72,13 @@ const parseStream = async function* (body, boundary) {
69
72
  .replace(/ \$/g, ' /$')
70
73
 
71
74
  // HACKS!!!
72
- // eslint-disable-next-line no-constant-condition
73
- while (true) {
74
- // ensure URLs start with slashes
75
- const m = changed.match(/(GET|PUT|POST|PATCH|DELETE) (\w)/)
76
- if (!m) break
77
- changed = changed.replace(m[0], `${m[1]} /${m[2]}`)
78
- // add content-length header
79
- try {
80
- let x = changed.substr(m.index)
81
- let y = x.indexOf(`${CRLF}HEAD`)
82
- let z = changed.substr(m.index, y)
83
- const lines = z.split(CRLF)
84
- if (lines.some(l => l.match(/^content-length/i))) continue
85
- const cl = lines.slice(-1)[0].length
86
- if (cl) {
87
- lines.splice(-2, 0, `content-length:${cl}`)
88
- changed = changed.substr(0, m.index) + lines.join(CRLF) + x.substr(y)
89
- }
90
- } catch (e) {
91
- console.error(e)
92
- }
93
- }
75
+ // ensure URLs start with slashes
76
+ changed = changed.replaceAll(/\r\n(GET|PUT|POST|PATCH|DELETE) (\w)/g, `\r\n$1 /$2`)
77
+ // add content-length headers
78
+ changed = changed.replaceAll(
79
+ /\r\n(.+)\r\nHEAD/g,
80
+ (match, p1) => `content-length: ${Buffer.byteLength(p1)}${CRLF}${match}`
81
+ )
94
82
  // remove strange "Group ID" appendix
95
83
  changed = changed.split(`${CRLF}Group ID`)[0] + CRLF
96
84