@sap/cds 9.3.1 → 9.4.2

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 (78) hide show
  1. package/CHANGELOG.md +54 -3
  2. package/_i18n/i18n_vi.properties +113 -0
  3. package/_i18n/messages.properties +106 -17
  4. package/_i18n/messages_ar.properties +194 -0
  5. package/_i18n/messages_bg.properties +194 -0
  6. package/_i18n/messages_cs.properties +194 -0
  7. package/_i18n/messages_da.properties +194 -0
  8. package/_i18n/messages_de.properties +194 -0
  9. package/_i18n/messages_el.properties +194 -0
  10. package/_i18n/messages_en.properties +194 -0
  11. package/_i18n/messages_en_US_saptrc.properties +194 -0
  12. package/_i18n/messages_es.properties +194 -0
  13. package/_i18n/messages_es_MX.properties +194 -0
  14. package/_i18n/messages_fi.properties +194 -0
  15. package/_i18n/messages_fr.properties +194 -0
  16. package/_i18n/messages_he.properties +194 -0
  17. package/_i18n/messages_hr.properties +194 -0
  18. package/_i18n/messages_hu.properties +194 -0
  19. package/_i18n/messages_it.properties +194 -0
  20. package/_i18n/messages_ja.properties +194 -0
  21. package/_i18n/messages_kk.properties +194 -0
  22. package/_i18n/messages_ko.properties +194 -0
  23. package/_i18n/messages_ms.properties +194 -0
  24. package/_i18n/messages_nl.properties +194 -0
  25. package/_i18n/messages_no.properties +194 -0
  26. package/_i18n/messages_pl.properties +194 -0
  27. package/_i18n/messages_pt.properties +194 -0
  28. package/_i18n/messages_ro.properties +194 -0
  29. package/_i18n/messages_ru.properties +194 -0
  30. package/_i18n/messages_sh.properties +194 -0
  31. package/_i18n/messages_sk.properties +194 -0
  32. package/_i18n/messages_sl.properties +194 -0
  33. package/_i18n/messages_sv.properties +194 -0
  34. package/_i18n/messages_th.properties +194 -0
  35. package/_i18n/messages_tr.properties +194 -0
  36. package/_i18n/messages_uk.properties +194 -0
  37. package/_i18n/messages_vi.properties +194 -0
  38. package/_i18n/messages_zh_CN.properties +194 -0
  39. package/_i18n/messages_zh_TW.properties +194 -0
  40. package/bin/serve.js +9 -1
  41. package/common.cds +9 -1
  42. package/lib/compile/cds-compile.js +1 -0
  43. package/lib/compile/etc/properties.js +1 -0
  44. package/lib/compile/for/flows.js +70 -4
  45. package/lib/compile/for/nodejs.js +1 -1
  46. package/lib/compile/minify.js +84 -56
  47. package/lib/compile/to/csn.js +2 -0
  48. package/lib/compile/to/yaml.js +1 -1
  49. package/lib/env/cds-requires.js +3 -0
  50. package/lib/i18n/bundles.js +8 -1
  51. package/lib/i18n/files.js +5 -1
  52. package/lib/i18n/index.js +1 -5
  53. package/lib/i18n/localize.js +4 -2
  54. package/lib/index.js +1 -1
  55. package/lib/ql/SELECT.js +16 -19
  56. package/lib/req/validate.js +10 -5
  57. package/lib/srv/bindings.js +1 -1
  58. package/lib/srv/cds-serve.js +1 -1
  59. package/lib/srv/middlewares/auth/ias-auth.js +3 -2
  60. package/lib/srv/middlewares/auth/jwt-auth.js +3 -2
  61. package/lib/srv/protocols/hcql.js +8 -6
  62. package/lib/srv/srv-dispatch.js +4 -8
  63. package/lib/srv/srv-handlers.js +28 -1
  64. package/lib/utils/colors.js +54 -49
  65. package/libx/_runtime/common/generic/flows.js +79 -12
  66. package/libx/_runtime/fiori/lean-draft.js +10 -2
  67. package/libx/_runtime/messaging/common-utils/connections.js +31 -18
  68. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  69. package/libx/_runtime/messaging/redis-messaging.js +1 -1
  70. package/libx/_runtime/ucl/Service.js +5 -5
  71. package/libx/http/body-parser.js +10 -1
  72. package/libx/odata/ODataAdapter.js +10 -7
  73. package/libx/odata/middleware/error.js +3 -0
  74. package/libx/odata/parse/afterburner.js +13 -16
  75. package/libx/odata/parse/multipartToJson.js +3 -1
  76. package/libx/rest/middleware/parse.js +1 -1
  77. package/package.json +1 -1
  78. package/server.js +1 -1
@@ -9,9 +9,14 @@ const TO = '@to'
9
9
  const FLOW_FROM = '@flow.from'
10
10
  const FLOW_TO = '@flow.to'
11
11
 
12
+ const FLOW_PREVIOUS = '$flow.previous'
13
+
12
14
  function buildAllowedCondition(action, statusElementName, statusEnum) {
13
15
  const fromList = getFrom(action)
14
- const conditions = fromList.map(from => `${statusElementName} = '${statusEnum[from].val ?? from}'`)
16
+ const conditions = fromList.map(from => {
17
+ const value = from['#'] ? (statusEnum[from['#']]?.val ?? statusEnum[from['#']]['$path'].at(-1)) : from
18
+ return `${statusElementName} = ${typeof value === 'string' ? `'${value}'` : value}`
19
+ })
15
20
  return `(${conditions.join(' OR ')})`
16
21
  }
17
22
 
@@ -26,14 +31,58 @@ async function checkStatus(req, action, statusElementName, statusEnum) {
26
31
  const allowed = await isCurrentStatusInFrom(req, action, statusElementName, statusEnum)
27
32
  if (!allowed) {
28
33
  const from = getFrom(action)
29
- req.reject({
34
+ const fromValues = JSON.stringify(from.flatMap(el => Object.values(el)))
35
+ cds.error({
30
36
  code: 409,
31
37
  message: from.length > 1 ? 'INVALID_FLOW_TRANSITION_MULTI' : 'INVALID_FLOW_TRANSITION_SINGLE',
32
- args: [action.name, statusElementName, from.join(',')]
38
+ args: [action.name, statusElementName, fromValues]
39
+ })
40
+ }
41
+ }
42
+
43
+ const buildUpKeys = parentKeys => {
44
+ const upKeys = {}
45
+ for (const key in parentKeys) {
46
+ upKeys[`up__${key}`] = parentKeys[key]
47
+ }
48
+ return upKeys
49
+ }
50
+
51
+ const updateFlowHistory = async (req, toValue, upKeys, changes, isPrevious) => {
52
+ if (cds.env.features.flows_history_stack && isPrevious) {
53
+ await DELETE.from(req.target.compositions['transitions_'].target).where({
54
+ timestamp: changes[changes.length - 1].timestamp
55
+ })
56
+ } else {
57
+ await INSERT.into(req.target.compositions['transitions_'].target).entries({
58
+ ...upKeys,
59
+ status: toValue
33
60
  })
34
61
  }
35
62
  }
36
63
 
64
+ const buildToKey = (action, statusEnum) => {
65
+ const to = action[TO] ?? action[FLOW_TO]
66
+ const toKey = to['#'] ? (statusEnum[to['#']].val ?? statusEnum[to['#']]['$path'].at(-1)) : to
67
+ return toKey
68
+ }
69
+
70
+ const handleStatusTransitionWithHistory = async (req, statusElementName, toKey, service) => {
71
+ let upKeys, changes
72
+ upKeys = buildUpKeys(req.params[0])
73
+ changes = await SELECT.from(req.target.compositions['transitions_'].target)
74
+ .where({ ...upKeys })
75
+ .orderBy('timestamp asc')
76
+ const isPrevious = toKey['='] === FLOW_PREVIOUS
77
+ if (isPrevious) {
78
+ if (changes.length <= 1)
79
+ return cds.error({ code: 409, message: 'No change has been made yet, cannot transition to previous status.' })
80
+ toKey = changes[changes.length - 2].status
81
+ }
82
+ await service.run(UPDATE(req.subject).with({ [statusElementName]: toKey }))
83
+ await updateFlowHistory(req, toKey, upKeys, changes, isPrevious)
84
+ }
85
+
37
86
  /**
38
87
  * handler registration
39
88
  */
@@ -54,9 +103,10 @@ module.exports = cds.service.impl(function () {
54
103
 
55
104
  let statusElement = Object.values(entity.elements).find(el => el[FLOW_STATUS])
56
105
  if (!statusElement) {
57
- cds.error(
58
- `Entity ${entity.name} does not have a status element, but its actions have registered @flow annotations.`
59
- )
106
+ cds.error({
107
+ code: 409,
108
+ message: `Entity ${entity.name} does not have a status element, but its actions have registered @flow annotations.`
109
+ })
60
110
  }
61
111
 
62
112
  let statusEnum, statusElementName
@@ -67,9 +117,10 @@ module.exports = cds.service.impl(function () {
67
117
  statusEnum = statusElement._target.elements['code'].enum
68
118
  statusElementName = statusElement.name + '_code'
69
119
  } else {
70
- cds.error(
71
- `Status element in entity ${entity.name} is not an enum and does not have a valid target with code enum.`
72
- )
120
+ cds.error({
121
+ code: 409,
122
+ message: `Status element in entity ${entity.name} is not an enum and does not have a valid target with code enum.`
123
+ })
73
124
  }
74
125
 
75
126
  entry.push({ events: fromActions, entity, statusElementName, statusEnum })
@@ -92,12 +143,28 @@ module.exports = cds.service.impl(function () {
92
143
  }
93
144
 
94
145
  for (const each of exit) {
146
+ async function handle_after_create(res, req) {
147
+ const parentKeys = Object.keys(req.target.keys)
148
+ const entry = {}
149
+ for (let i = 0; i < parentKeys.length; i++) {
150
+ entry[`up__${parentKeys[i]}`] = req.data[parentKeys[i]]
151
+ }
152
+ await INSERT.into(req.target.compositions['transitions_'].target).entries({
153
+ ...entry,
154
+ status: res[each.statusElementName]
155
+ })
156
+ }
157
+ if ('transitions_' in (each.entity.compositions ?? {})) this.after('CREATE', each.entity, handle_after_create)
158
+
95
159
  async function handle_exit_state(req, next) {
96
160
  const res = await next()
97
161
  const action = req.target.actions[req.event]
98
- const to = action[TO] ?? action[FLOW_TO]
99
- const toKey = to['#'] ?? to['='] ?? to
100
- await UPDATE(req.subject).with({ [each.statusElementName]: each.statusEnum[toKey].val ?? toKey })
162
+ let toKey = buildToKey(action, each.statusEnum)
163
+ if ('transitions_' in (req.target.compositions ?? {})) {
164
+ await handleStatusTransitionWithHistory(req, each.statusElementName, toKey, this)
165
+ } else {
166
+ await this.run(UPDATE(req.subject).with({ [each.statusElementName]: toKey }))
167
+ }
101
168
  return res
102
169
  }
103
170
  this.on(each.events, each.entity, handle_exit_state)
@@ -438,6 +438,8 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
438
438
  message.additionalTargets = message.additionalTargets?.map(t => (t.startsWith('in/') ? t.slice(3) : t))
439
439
  delete message['@Common.additionalTargets']
440
440
 
441
+ if (!message.target) return acc //> silently ignore messages without target
442
+
441
443
  // Handle validation messages produced during draftActivate, that went through error normalization already
442
444
  // > We must not store pre-localized data in DraftAdministrativeData.DraftMessages
443
445
  const messageTarget = message.target.startsWith('in/') ? message.target.slice(3) : message.target
@@ -580,7 +582,12 @@ const handle = async function (req) {
580
582
  if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
581
583
  if (_req.target.isDraft && (_req.event === 'UPDATE' || _req.event === 'NEW')) {
582
584
  // Degrade all errors to messages & prevent !!req.errors into req.reject() in dispatch
583
- _req.error = (...args) => _req._messages.add(4, ...args)
585
+ const originalError = _req.error.bind(_req)
586
+ _req.error = (...args) => {
587
+ // REVISIT: re-consider target variants
588
+ if (args[0]?.target || args[2]) return _req._messages.add(4, ...args)
589
+ return originalError(...args)
590
+ }
584
591
  }
585
592
  }
586
593
 
@@ -910,10 +917,11 @@ const handle = async function (req) {
910
917
  if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && _req.errors)
911
918
  _req.on('failed', async () => {
912
919
  const nextDraftMessages = _compileUpdatedDraftMessages(
920
+ // REVISIT: e._message hack for draft validation messages
913
921
  // Errors procesed during 'failed' will have undergone error._normalize at this point
914
922
  // > We need to revert the code - message swap _normalize includes
915
923
  // > This is required to ensure, no localized messages are persisted and redundant localization is avoided
916
- _req.errors.map(e => ({ message: e.code ?? e.message, target: e.target, args: e.args, i18n: e.i18n })),
924
+ _req.errors.map(e => ({ message: e._message ?? e.message, target: e.target, args: e.args, i18n: e.i18n })),
917
925
  persistedDraftMessages,
918
926
  {},
919
927
  draftRef
@@ -1,17 +1,31 @@
1
1
  const { expBkfRnd: waitingTime } = require('../../common/utils/waitingTime')
2
2
 
3
+ const _rmHandlers = client => {
4
+ client.removeAllListeners('connected')
5
+ client.removeAllListeners('error')
6
+ client.removeAllListeners('disconnected')
7
+ }
8
+
3
9
  const _connectUntilConnected = (client, LOG, x) => {
10
+ if (client._reconnecting) return
11
+ client._reconnecting = true
12
+
4
13
  const _waitingTime = waitingTime(x)
5
14
  setTimeout(() => {
6
15
  connect(client, LOG, true)
7
16
  .then(() => {
17
+ client._reconnecting = false
8
18
  LOG._warn && LOG.warn('Reconnected to Enterprise Messaging Client')
9
19
  })
10
- .catch(() => {
20
+ .catch(e => {
21
+ _rmHandlers(client)
22
+ LOG.error(e)
23
+
11
24
  LOG._warn &&
12
25
  LOG.warn(
13
26
  `Connection to Enterprise Messaging Client lost: Reconnecting in ${Math.round(_waitingTime / 1000)} s`
14
27
  )
28
+ client._reconnecting = false
15
29
  _connectUntilConnected(client, LOG, x + 1)
16
30
  })
17
31
  }, _waitingTime)
@@ -19,35 +33,36 @@ const _connectUntilConnected = (client, LOG, x) => {
19
33
 
20
34
  const connect = (client, LOG, keepAlive) => {
21
35
  return new Promise((resolve, reject) => {
36
+ _rmHandlers(client)
37
+
22
38
  client
23
39
  .once('connected', function () {
24
- client.removeAllListeners('error')
40
+ const handleReconnection = err => {
41
+ if (client._reconnecting) return
25
42
 
26
- client.once('error', err => {
27
- if (LOG._error) {
43
+ if (err && LOG._error) {
28
44
  err.message = 'Client error: ' + err.message
29
45
  LOG.error(err)
30
46
  }
31
47
  if (keepAlive) {
32
- client.removeAllListeners('error')
33
- client.removeAllListeners('connected')
48
+ _rmHandlers(client)
34
49
  _connectUntilConnected(client, LOG, 0)
35
50
  }
36
- })
51
+ }
37
52
 
53
+ _rmHandlers(client)
54
+ client.once('error', handleReconnection)
38
55
  if (keepAlive) {
39
- client.once('disconnected', () => {
40
- client.removeAllListeners('error')
41
- client.removeAllListeners('connected')
42
- _connectUntilConnected(client, LOG, 0)
43
- })
56
+ client.once('disconnected', handleReconnection)
44
57
  }
45
58
 
46
- resolve(this)
59
+ resolve(client)
47
60
  })
48
61
  .once('error', err => {
49
- client.removeAllListeners('connected')
50
- reject(err)
62
+ _rmHandlers(client)
63
+ const e = new Error('Connection error')
64
+ e.cause = err
65
+ reject(e)
51
66
  })
52
67
 
53
68
  client.connect()
@@ -56,9 +71,7 @@ const connect = (client, LOG, keepAlive) => {
56
71
 
57
72
  const disconnect = client => {
58
73
  return new Promise((resolve, reject) => {
59
- client.removeAllListeners('disconnected')
60
- client.removeAllListeners('connected')
61
- client.removeAllListeners('error')
74
+ _rmHandlers(client)
62
75
 
63
76
  client.once('disconnected', () => {
64
77
  client.removeAllListeners('error')
@@ -102,8 +102,8 @@ class EndpointRegistry {
102
102
  const queues = req.body && !_isAll(req.body.queues) && req.body.queues
103
103
  const options = { wipeData: req.body && req.body.wipeData }
104
104
 
105
- if (tenants && !Array.isArray(tenants)) res.send(400).send('Request parameter `tenants` must be an array.')
106
- if (queues && !Array.isArray(queues)) res.send(400).send('Request parameter `queues` must be an array.')
105
+ if (tenants && !Array.isArray(tenants)) res.status(400).send('Request parameter `tenants` must be an array.')
106
+ if (queues && !Array.isArray(queues)) res.status(400).send('Request parameter `queues` must be an array.')
107
107
 
108
108
  const tenantInfo = tenants ? await Promise.all(tenants.map(t => getTenantInfo(t))) : await getTenantInfo()
109
109
 
@@ -9,7 +9,7 @@ const _handleReconnects = (client, LOG) => {
9
9
  })
10
10
 
11
11
  client.on('error', error => {
12
- LOG.warn('Failed to connect to Redis: ', error)
12
+ LOG.warn('Failed to connect to Redis:', error)
13
13
  })
14
14
  }
15
15
 
@@ -82,8 +82,8 @@ module.exports = class UCLService extends cds.Service {
82
82
  .map(token => token.trim())
83
83
  .every(token => reqClientCertSubjectTokens.includes(token))
84
84
  if (!matchesUclInfoSubject) {
85
- LOG.debug('Received Request Subject Info: ', reqClientCertSubject)
86
- LOG.debug('Expected UCL Subject Info: ', trustedCertSubject)
85
+ LOG.debug('Received Request Subject Info:', reqClientCertSubject)
86
+ LOG.debug('Expected UCL Subject Info:', trustedCertSubject)
87
87
  throw new cds.error(401, 'Received .cert subject does not match trusted UCL info subject')
88
88
  }
89
89
  const matchesUclInfoIssuer = trustedCertIssuer
@@ -91,8 +91,8 @@ module.exports = class UCLService extends cds.Service {
91
91
  .map(token => token.trim())
92
92
  .every(token => reqClientCertIssuerTokens.includes(token))
93
93
  if (!matchesUclInfoIssuer) {
94
- LOG.debug('Received Request Issuer Info: ', reqClientCertIssuer)
95
- LOG.debug('Expected UCL Issuer Info: ', trustedCertIssuer)
94
+ LOG.debug('Received Request Issuer Info:', reqClientCertIssuer)
95
+ LOG.debug('Expected UCL Issuer Info:', trustedCertIssuer)
96
96
  throw new cds.error(401, 'Received .cert issuer does not match trusted UCL info issuer')
97
97
  }
98
98
  }
@@ -104,7 +104,7 @@ module.exports = class UCLService extends cds.Service {
104
104
  if (req.headers.location)
105
105
  throw new cds.error(400, 'Location header found in tenant mapping notification: Async flow not supported!')
106
106
 
107
- LOG.debug('Tenant mapping notification: ', req.data)
107
+ LOG.debug('Tenant mapping notification:', req.data)
108
108
 
109
109
  const { operation } = req.data?.context ?? {}
110
110
  if (operation !== 'assign' && operation !== 'unassign')
@@ -3,6 +3,8 @@ const express = require('express')
3
3
  // basically express.json() with string representation of body stored in req._raw for recovery
4
4
  // REVISIT: why do we need our own body parser? Only because of req._raw?
5
5
  module.exports = function bodyParser4(adapter, options = {}) {
6
+ const express5 = !express.application.del
7
+
6
8
  Object.assign(options, adapter.body_parser_options)
7
9
  options.type ??= 'json' // REVISIT: why do we need to override type here?
8
10
  const textParser = express.text(options)
@@ -13,8 +15,15 @@ module.exports = function bodyParser4(adapter, options = {}) {
13
15
  return next()
14
16
  }
15
17
  textParser(req, res, function http_body_parser_next(err) {
18
+ // REVISIT: content-length > 0 but empty body is not an error with express^5
19
+ if (!err && express5 && !req.body && req.headers['content-length'] > 0) {
20
+ err = new Error('request aborted')
21
+ err.code = 'ECONNABORTED'
22
+ err.status = err.statusCode = 400
23
+ }
24
+
16
25
  if (err) return next(Object.assign(err, { statusCode: 400 }))
17
- if (typeof req.body !== 'string') return next()
26
+ if (typeof req.body !== 'string' && req.body !== undefined) return next()
18
27
 
19
28
  req._raw = req.body || '{}'
20
29
  try {
@@ -1,6 +1,8 @@
1
1
  const cds = require('../../lib')
2
2
  const LOG = cds.log('odata')
3
3
 
4
+ const express = require('express')
5
+
4
6
  const HttpAdapter = require('../../lib/srv/protocols/http')
5
7
  const HttpRequest = require('../http/HttpRequest')
6
8
  const bodyParser4 = require('../http/body-parser')
@@ -32,6 +34,7 @@ module.exports = class ODataAdapter extends HttpAdapter {
32
34
  next()
33
35
  }
34
36
 
37
+ const jsonBodyParser = bodyParser4(this)
35
38
  function validate_representation_headers(req, res, next) {
36
39
  if (req.method === 'PUT' && isStream(req._query)) {
37
40
  req.body = { value: req }
@@ -69,7 +72,7 @@ module.exports = class ODataAdapter extends HttpAdapter {
69
72
  next(operation ? { code: 405 } : undefined)
70
73
  }
71
74
 
72
- const jsonBodyParser = bodyParser4(this)
75
+ const wildcard = express.application.del ? '*' : '{*splat}'
73
76
  return (
74
77
  super.router
75
78
  .use(set_odata_version)
@@ -83,13 +86,13 @@ module.exports = class ODataAdapter extends HttpAdapter {
83
86
  // .all is used deliberately instead of .use so that the matched path is not stripped from req properties
84
87
  .all('/\\$batch', require('./middleware/batch')(this))
85
88
  // handle
86
- .head('*', (_, res) => res.sendStatus(405))
87
- .post('*', operation4(this), create4(this))
88
- .get('*', operation4(this), stream4(this), read4(this))
89
+ .head(wildcard, (_, res) => res.sendStatus(405))
90
+ .post(wildcard, operation4(this), create4(this))
91
+ .get(wildcard, operation4(this), stream4(this), read4(this))
89
92
  .use(validate_operation_http_method)
90
- .put('*', update4(this), create4(this, 'upsert'))
91
- .patch('*', update4(this), create4(this, 'upsert'))
92
- .delete('*', delete4(this))
93
+ .put(wildcard, update4(this), create4(this, 'upsert'))
94
+ .patch(wildcard, update4(this), create4(this, 'upsert'))
95
+ .delete(wildcard, delete4(this))
93
96
  // error
94
97
  .use(error4(this))
95
98
  )
@@ -60,6 +60,8 @@ const _normalize = (err, req, keep,
60
60
  const msg = i18n.messages.at (key, '', err.args) // lookup messages for log output from factory default texts
61
61
  if (msg && msg !== key) {
62
62
  if (typeof err.code !== 'string') err.code = key
63
+ // REVISIT: e._message hack for draft validation messages
64
+ Object.defineProperty(err, '_message', { value: err.message })
63
65
  err.message = msg
64
66
  }
65
67
  if (typeof err.code !== 'string') err.code = String(err.code ?? status ?? '')
@@ -77,6 +79,7 @@ const _normalize = (err, req, keep,
77
79
  if (!that.message) that.message = this.message
78
80
  return that
79
81
  }})
82
+
80
83
  return status
81
84
  }
82
85
 
@@ -650,11 +650,12 @@ function _processColumns(cqn, target, protocol) {
650
650
  }
651
651
  const prefixRef = processedColumnRef.slice(0, processedColumnRef.length - 1)
652
652
  const aggregatedElementRef = [...prefixRef, aggregatedPropertyName]
653
- const isCurrencyCodeOrUnitOfMeasure = !!(aggregatedElement[SMTCS_CC] || aggregatedElement[SMTCS_UOM])
654
653
  processedColumn.as = processedColumn.as || aggregatedPropertyName
655
654
 
656
- // Specifically handle aggregating semantic amounts
655
+ const isCurrencyCodeOrUnitOfMeasure =
656
+ aggregatedElement && !!(aggregatedElement[SMTCS_CC] || aggregatedElement[SMTCS_UOM])
657
657
  if (isCurrencyCodeOrUnitOfMeasure) {
658
+ // Specifically handle aggregating semantic amounts
658
659
  columns[i] = {
659
660
  xpr: [
660
661
  'case',
@@ -772,13 +773,13 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
772
773
  const ignoredColumns = Object.values(target?.elements ?? {})
773
774
  .filter(element => element['@cds.api.ignore'] && !element.isAssociation)
774
775
  .map(element => element.name)
775
- const _aliases = []
776
+ const newFoundAliases = []
776
777
 
777
778
  for (const x of xpr) {
778
- if (x.as) _aliases.push(x.as)
779
+ if (x.as) newFoundAliases.push(x.as)
779
780
 
780
781
  if (x.xpr) {
781
- _validateXpr(x.xpr, target, isOne, model)
782
+ _validateXpr(x.xpr, target, isOne, model, aliases)
782
783
  continue
783
784
  }
784
785
 
@@ -793,13 +794,11 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
793
794
  _validateXpr(x.ref[0].where, element._target ?? element.items, isOne, model)
794
795
  }
795
796
 
796
- if (!target?.elements) {
797
- _doesNotExistError(false, refName, target.name, target.kind)
797
+ if (ignoredColumns.includes(refName) || (!target?.elements?.[refName] && !aliases.includes(refName))) {
798
+ _doesNotExistError(x.expand, refName, target.name, target.kind)
798
799
  }
799
800
 
800
- if (ignoredColumns.includes(refName) || (!target.elements[refName] && !aliases.includes(refName))) {
801
- _doesNotExistError(x.expand, refName, target.name)
802
- } else if (x.ref.length > 1) {
801
+ if (x.ref.length > 1) {
803
802
  const element = target.elements[refName]
804
803
  if (element.isAssociation) {
805
804
  // navigation
@@ -833,7 +832,7 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
833
832
  }
834
833
 
835
834
  if (x.func) {
836
- _validateXpr(x.args, target, isOne, model)
835
+ _validateXpr(x.args, target, isOne, model, aliases)
837
836
  continue
838
837
  }
839
838
 
@@ -843,7 +842,7 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
843
842
  }
844
843
  }
845
844
 
846
- return _aliases
845
+ return newFoundAliases
847
846
  }
848
847
 
849
848
  function _validateQuery(SELECT, target, isOne, model) {
@@ -851,12 +850,10 @@ function _validateQuery(SELECT, target, isOne, model) {
851
850
 
852
851
  if (SELECT.from.SELECT) {
853
852
  const { target } = targetFromPath(SELECT.from.SELECT.from, model)
854
- const subselectAliases = _validateQuery(SELECT.from.SELECT, target, SELECT.from.SELECT.one, model)
855
- aliases.push(...subselectAliases)
853
+ aliases.push(..._validateQuery(SELECT.from.SELECT, target, SELECT.from.SELECT.one, model))
856
854
  }
857
855
 
858
- const columnAliases = _validateXpr(SELECT.columns, target, isOne, model)
859
- aliases.push(...columnAliases)
856
+ aliases.push(..._validateXpr(SELECT.columns, target, isOne, model, aliases))
860
857
 
861
858
  _validateXpr(SELECT.orderBy, target, isOne, model, aliases)
862
859
  _validateXpr(SELECT.where, target, isOne, model, aliases)
@@ -117,7 +117,9 @@ const _parseStream = async function* (body, boundary) {
117
117
  .replace(/^--(.*)$/gm, (_, g) => `HEAD /${g} HTTP/1.1${g.slice(-2) === '--' ? CRLF : ''}`)
118
118
  // correct content-length for non-HEAD requests is inserted below
119
119
  .replace(/content-length: \d+\r\n/gim, '') // if content-length is given it should be taken
120
- .replace(/ \$/g, ' /$')
120
+ .replaceAll(/^(?:(GET|PUT|POST|PATCH|DELETE)).*(?:\r?\n(?!\r?\n).*)*/gim, block => {
121
+ return block.replaceAll(/ \$/g, ' /$')
122
+ })
121
123
 
122
124
  // HACKS!!!
123
125
  // ensure URLs start with slashes
@@ -120,7 +120,7 @@ exports.parse = function (req, res, next) {
120
120
  if (operation && (operation.kind === 'action' || operation.kind === 'function') && !operation.params) {
121
121
  req._data = {}
122
122
  } else {
123
- const payload = args || req.body
123
+ const payload = args || req.body || {}
124
124
  if (!operation) Object.assign(payload, keys)
125
125
  preProcessData(payload, service, definition)
126
126
  req._data = payload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "9.3.1",
3
+ "version": "9.4.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
package/server.js CHANGED
@@ -48,7 +48,7 @@ module.exports = async function cds_server (options) {
48
48
  if (o.index) app.get ('/',o.index) //> if none in ./app
49
49
 
50
50
  // load and prepare models
51
- const csn = await cds.load(o.from||'*',o)
51
+ const csn = await cds.load(o.from||'*',o); o.from = false
52
52
  cds.edmxs = cds.compile.to.edmx.files (csn)
53
53
  cds.model = cds.compile.for.nodejs (csn)
54
54