@sap/cds 7.8.2 → 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 (136) hide show
  1. package/CHANGELOG.md +37 -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/entities.js +10 -0
  35. package/lib/linked/models.js +1 -1
  36. package/lib/plugins.js +1 -1
  37. package/lib/ql/INSERT.js +17 -3
  38. package/lib/ql/Query.js +4 -0
  39. package/lib/ql/infer.js +1 -1
  40. package/lib/req/request.js +1 -1
  41. package/lib/srv/cds-serve.js +1 -0
  42. package/lib/srv/middlewares/cds-context.js +1 -1
  43. package/lib/srv/protocols/odata-v4.js +5 -6
  44. package/lib/srv/srv-models.js +9 -2
  45. package/lib/utils/cds-test.js +2 -0
  46. package/lib/utils/cds-utils.js +9 -4
  47. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
  52. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
  53. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
  55. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
  60. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
  64. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
  65. package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
  66. package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
  67. package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
  68. package/libx/_runtime/common/generic/auth/index.js +2 -0
  69. package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
  70. package/libx/_runtime/common/generic/auth/restrict.js +6 -5
  71. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  72. package/libx/_runtime/common/generic/crud.js +5 -8
  73. package/libx/_runtime/common/generic/etag.js +8 -6
  74. package/libx/_runtime/common/generic/sorting.js +2 -2
  75. package/libx/_runtime/common/i18n/messages.properties +1 -0
  76. package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
  77. package/libx/_runtime/common/utils/compareJson.js +274 -0
  78. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  79. package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
  80. package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
  81. package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
  82. package/libx/_runtime/common/utils/resolveView.js +0 -16
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  84. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  85. package/libx/_runtime/common/utils/streamProp.js +9 -2
  86. package/libx/_runtime/common/utils/ucsn.js +1 -1
  87. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  88. package/libx/_runtime/db/generic/rewrite.js +7 -13
  89. package/libx/_runtime/fiori/generic/activate.js +1 -1
  90. package/libx/_runtime/fiori/generic/edit.js +1 -1
  91. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  92. package/libx/_runtime/fiori/lean-draft.js +151 -46
  93. package/libx/_runtime/fiori/utils/handler.js +1 -1
  94. package/libx/_runtime/hana/execute.js +6 -2
  95. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  96. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  97. package/libx/_runtime/messaging/event-broker.js +212 -0
  98. package/libx/_runtime/remote/Service.js +9 -32
  99. package/libx/_runtime/remote/utils/client.js +13 -21
  100. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
  101. package/libx/_runtime/sqlite/execute.js +8 -3
  102. package/libx/_runtime/ucl/Service.js +259 -0
  103. package/libx/common/assert/index.js +5 -11
  104. package/libx/common/assert/validation.js +6 -1
  105. package/libx/odata/index.js +47 -25
  106. package/libx/odata/middleware/batch.js +8 -7
  107. package/libx/odata/middleware/create.js +42 -16
  108. package/libx/odata/middleware/delete.js +18 -11
  109. package/libx/odata/middleware/metadata.js +15 -14
  110. package/libx/odata/middleware/operation.js +30 -40
  111. package/libx/odata/middleware/parse.js +2 -3
  112. package/libx/odata/middleware/read.js +59 -52
  113. package/libx/odata/middleware/service-document.js +7 -7
  114. package/libx/odata/middleware/stream.js +26 -24
  115. package/libx/odata/middleware/update.js +53 -92
  116. package/libx/odata/parse/afterburner.js +45 -47
  117. package/libx/odata/parse/grammar.peggy +3 -3
  118. package/libx/odata/parse/multipartToJson.js +10 -22
  119. package/libx/odata/parse/parser.js +1 -1
  120. package/libx/odata/utils/etag.js +13 -0
  121. package/libx/odata/utils/handler.js +120 -0
  122. package/libx/odata/utils/index.js +15 -2
  123. package/libx/odata/utils/metaInfo.js +410 -0
  124. package/libx/odata/utils/path.js +5 -2
  125. package/libx/odata/utils/readAfterWrite.js +23 -0
  126. package/libx/odata/utils/result.js +4 -5
  127. package/libx/rest/RestAdapter.js +4 -13
  128. package/libx/rest/middleware/parse.js +40 -7
  129. package/package.json +1 -1
  130. package/server.js +1 -0
  131. package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
  132. package/libx/_runtime/common/utils/thenable.js +0 -51
  133. package/libx/_runtime/rest/service.js +0 -2
  134. package/libx/odata/parse/parseToCqn.js +0 -39
  135. package/libx/rest/middleware/input.js +0 -54
  136. package/libx/rest/middleware/payload.js +0 -13
@@ -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