@sap/cds 8.8.3 → 8.9.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 (73) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/_i18n/i18n_en_US_saptrc.properties +3 -0
  3. package/bin/colors.js +2 -0
  4. package/bin/test.js +103 -75
  5. package/eslint.config.mjs +16 -4
  6. package/lib/compile/for/lean_drafts.js +4 -0
  7. package/lib/compile/parse.js +26 -6
  8. package/lib/env/cds-env.js +3 -1
  9. package/lib/env/cds-requires.js +0 -3
  10. package/lib/env/schemas/cds-rc.js +11 -0
  11. package/lib/log/format/aspects/cls.js +2 -1
  12. package/lib/log/format/json.js +1 -1
  13. package/lib/plugins.js +2 -3
  14. package/lib/ql/SELECT.js +2 -1
  15. package/lib/ql/cds-ql.js +2 -0
  16. package/lib/ql/cds.ql-predicates.js +6 -4
  17. package/lib/ql/resolve.js +46 -0
  18. package/lib/req/validate.js +1 -0
  19. package/lib/srv/bindings.js +64 -43
  20. package/lib/srv/cds-connect.js +1 -1
  21. package/lib/srv/cds-serve.js +2 -2
  22. package/lib/srv/middlewares/auth/ias-auth.js +2 -0
  23. package/lib/srv/protocols/http.js +2 -2
  24. package/lib/srv/protocols/index.js +1 -1
  25. package/lib/srv/protocols/odata-v4.js +0 -1
  26. package/lib/srv/srv-tx.js +1 -1
  27. package/lib/test/cds-test.js +3 -4
  28. package/lib/utils/cds-utils.js +19 -19
  29. package/lib/utils/colors.js +46 -45
  30. package/lib/utils/csv-reader.js +5 -5
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -2
  32. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  33. package/libx/_runtime/common/Service.js +4 -2
  34. package/libx/_runtime/common/composition/data.js +1 -2
  35. package/libx/_runtime/common/composition/tree.js +6 -4
  36. package/libx/_runtime/common/generic/sorting.js +6 -2
  37. package/libx/_runtime/common/utils/cqn2cqn4sql.js +6 -7
  38. package/libx/_runtime/common/utils/differ.js +1 -1
  39. package/libx/_runtime/common/utils/draft.js +1 -1
  40. package/libx/_runtime/common/utils/foreignKeyPropagations.js +6 -2
  41. package/libx/_runtime/common/utils/keys.js +13 -84
  42. package/libx/_runtime/common/utils/propagateForeignKeys.js +4 -3
  43. package/libx/_runtime/common/utils/resolveView.js +96 -102
  44. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  45. package/libx/_runtime/common/utils/stream.js +2 -3
  46. package/libx/_runtime/db/utils/columns.js +1 -1
  47. package/libx/_runtime/fiori/lean-draft.js +11 -7
  48. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  49. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -2
  50. package/libx/_runtime/messaging/file-based.js +6 -6
  51. package/libx/_runtime/messaging/kafka.js +5 -7
  52. package/libx/_runtime/messaging/redis-messaging.js +1 -1
  53. package/libx/_runtime/messaging/service.js +11 -4
  54. package/libx/_runtime/remote/Service.js +13 -5
  55. package/libx/_runtime/remote/utils/client.js +1 -0
  56. package/libx/_runtime/ucl/Service.js +135 -126
  57. package/libx/common/utils/path.js +34 -22
  58. package/libx/odata/middleware/create.js +2 -0
  59. package/libx/odata/middleware/operation.js +8 -2
  60. package/libx/odata/middleware/parse.js +1 -1
  61. package/libx/odata/middleware/stream.js +1 -2
  62. package/libx/odata/middleware/update.js +2 -0
  63. package/libx/odata/parse/afterburner.js +17 -9
  64. package/libx/odata/parse/cqn2odata.js +43 -22
  65. package/libx/odata/parse/grammar.peggy +21 -19
  66. package/libx/odata/parse/parser.js +1 -1
  67. package/libx/odata/utils/metadata.js +8 -2
  68. package/libx/odata/utils/odataBind.js +36 -0
  69. package/libx/outbox/index.js +1 -0
  70. package/libx/rest/middleware/operation.js +9 -8
  71. package/libx/rest/middleware/parse.js +1 -0
  72. package/package.json +3 -3
  73. package/lib/i18n/resources.js +0 -150
@@ -77,7 +77,7 @@ class RedisMessaging extends cds.MessagingService {
77
77
  const msg = normalizeIncomingMessage(message)
78
78
  msg.event = topic
79
79
  try {
80
- await this.tx({ user: cds.User.privileged }, tx => tx.emit(msg))
80
+ await this.processInboundMsg({}, msg)
81
81
  } catch (e) {
82
82
  e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
83
83
  this.LOG.error(e)
@@ -78,15 +78,22 @@ class MessagingService extends cds.Service {
78
78
 
79
79
  async handle(msg) {
80
80
  if (msg.inbound) {
81
- if (cds.model) {
82
- const ctx = cds.context
83
- ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
84
- }
85
81
  return super.handle(this.message4(msg))
86
82
  }
87
83
  return super.handle(msg)
88
84
  }
89
85
 
86
+ async processInboundMsg(ctx, msg) {
87
+ msg.inbound = true
88
+ if (!cds.context) cds.context = {}
89
+ if (ctx.tenant) cds.context.tenant = ctx.tenant
90
+ if (!ctx.user) ctx.user = cds.User.privileged
91
+ // this.tx expects cds.context.model
92
+ if (cds.model && (cds.env.requires.extensibility || cds.env.requires.toggles))
93
+ cds.context.model = await ExtendedModels.model4(ctx.tenant, ctx.features || {})
94
+ return await this.tx(ctx, tx => tx.emit(msg))
95
+ }
96
+
90
97
  on(event, cb) {
91
98
  const _event = _warnAndStripTopicPrefix(event)
92
99
  // save all subscribed topics (not needed for local-messaging)
@@ -3,12 +3,13 @@ const cds = require('../cds')
3
3
  const { run, getReqOptions } = require('./utils/client')
4
4
  const { getCloudSdk, getCloudSdkConnectivity, getCloudSdkResilience } = require('./utils/cloudSdkProvider')
5
5
  const { hasAliasedColumns } = require('./utils/data')
6
- const { resolveView, getTransition, findQueryTarget } = require('../common/utils/resolveView')
6
+ const { findQueryTarget } = require('../common/utils/resolveView')
7
7
  const postProcess = require('../common/utils/postProcess')
8
8
  const { formatVal } = require('../../odata/utils')
9
9
 
10
- const _setHeaders = (defaultHeaders, req) => {
10
+ const _getHeaders = (defaultHeaders, req) => {
11
11
  return Object.assign(
12
+ {},
12
13
  defaultHeaders,
13
14
  Object.keys(req.headers).reduce((acc, cur) => {
14
15
  acc[cur.toLowerCase()] = req.headers[cur]
@@ -254,10 +255,16 @@ class RemoteService extends cds.Service {
254
255
  throw new Error(`"url" or "destination" property must be configured in "credentials" of "${this.name}".`)
255
256
 
256
257
  const reqOptions = getReqOptions(req, query, this)
257
- reqOptions.headers = _setHeaders(reqOptions.headers, req)
258
+ reqOptions.headers = _getHeaders(reqOptions.headers, req)
259
+
260
+ // ensure request correlation (even with systems that use x-correlationid)
261
+ const correlationId = reqOptions.headers['x-correlation-id'] || cds.context?.id //> prefer custom header over context id
262
+ reqOptions.headers['x-correlation-id'] = correlationId
263
+ reqOptions.headers['x-correlationid'] = correlationId
258
264
 
259
265
  const { kind, destination, destinationOptions } = this
260
- const resolvedTarget = resolvedTargetOfQuery(query) || getTransition(req.target, this).target
266
+ const resolvedTarget =
267
+ resolvedTargetOfQuery(query) || cds.ql.resolve.transitions(query, this)?.target || req.target
261
268
  const returnType = req._returnType
262
269
  const additionalOptions = { destination, kind, resolvedTarget, returnType, destinationOptions }
263
270
 
@@ -289,7 +296,8 @@ class RemoteService extends cds.Service {
289
296
  // we need to post process if alias was explicitly set in query
290
297
  if (_isSelectWithAliasedColumns(req.query)) result = postProcess(req.query, result, this, true)
291
298
  } else {
292
- const query = resolveView(req.query, this.model, this)
299
+ const query = cds.ql.resolve(req.query, this)
300
+ if (!query) throw new Error(`Target ${req.target.name} cannot be resolved for service ${this.name}`)
293
301
  const target = findQueryTarget(query) || req.target
294
302
  // we need to provide target explicitly because it's cached within ensure_target
295
303
  const _req = new cds.Request({ query, target, _resolved: true, headers: req.headers, method: req.method })
@@ -73,6 +73,7 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
73
73
 
74
74
  // set `fetchCsrfToken` to `false` because we mount a custom CSRF middleware
75
75
  const requestOptions = { fetchCsrfToken: false }
76
+
76
77
  return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
77
78
  }
78
79
 
@@ -1,18 +1,18 @@
1
1
  const cds = require('../cds')
2
2
  const LOG = cds.log('ucl')
3
- const fs = require('fs').promises
4
3
 
4
+ const fs = require('fs').promises
5
5
  const https = require('https')
6
6
 
7
7
  class UCLService extends cds.Service {
8
8
  async init() {
9
9
  await super.init()
10
10
 
11
- for (const _required of ['namespace', 'systemType', 'systemDescription']) {
12
- if (!this.options[_required])
13
- throw new Error(
14
- `The UCL service requires mandatory parameter \`${_required}\`, please provide it as described in the documentation.`
15
- )
11
+ this._applicationTemplate = _getApplicationTemplate(this.options)
12
+ if (!this._applicationTemplate.applicationNamespace) {
13
+ throw new Error(
14
+ 'The UCL service requires a valid `applicationTemplate`, please provide it as described in the documentation.'
15
+ )
16
16
  }
17
17
 
18
18
  if (!cds.requires.multitenancy && cds.env.profile !== 'mtx-sidecar')
@@ -50,99 +50,6 @@ class UCLService extends cds.Service {
50
50
  })
51
51
  }
52
52
 
53
- async readTemplate() {
54
- const xsappname = this.options.credentials.xsappname
55
- const query = `
56
- query ($key: String!, $value: String!) {
57
- applicationTemplates(filter: { key: $key, query: $value }) {
58
- data {
59
- id
60
- name
61
- description
62
- placeholders {
63
- name
64
- description
65
- }
66
- applicationInput
67
- labels
68
- webhooks {
69
- type
70
- }
71
- }
72
- }
73
- }
74
- `
75
- const variables = { key: 'xsappname', value: `"${xsappname}"` }
76
- const res = await this._request(query, variables)
77
- if (res) return res.applicationTemplates.data[0]
78
- }
79
-
80
- async createTemplate() {
81
- const xsappname = this.options.credentials.xsappname
82
- const query = `mutation {
83
- result: createApplicationTemplate (
84
- in: {
85
- name: "${this.options.systemType}"
86
- description: "${this.options.systemDescription}"
87
- applicationInput: {
88
- name: "${this.options.systemType}"
89
- description: "${this.options.systemDescription}"
90
- providerName: "${this.options.provider}"
91
- localTenantID: "{{tenant-id}}"
92
- labels: {
93
- displayName: "{{subdomain}}"
94
- }
95
- }
96
- placeholders: [
97
- { name: "subdomain", description: "The subdomain of the consumer tenant" }
98
- { name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.subscribedTenantId" }
99
- ]
100
- labels: {
101
- managed_app_provisioning: true
102
- xsappname: "${xsappname}"
103
- }
104
- applicationNamespace: "${this.options.namespace}"
105
- accessLevel: GLOBAL
106
- }
107
- ) {
108
- id
109
- name
110
- labels
111
- applicationInput
112
- applicationNamespace
113
- }
114
- }`
115
- try {
116
- return this._handleResponse(await this._request(query))
117
- } catch (e) {
118
- this._handleResponse(e)
119
- }
120
- }
121
-
122
- _handleResponse(result) {
123
- if (result.response && result.response.errors) {
124
- let errorMessage = result.response.errors[0].message
125
- throw new Error(errorMessage)
126
- } else {
127
- return result.result
128
- }
129
- }
130
-
131
- async deleteTemplate() {
132
- const template = await this.readTemplate()
133
- if (!template) return
134
- const query = `mutation {
135
- result: deleteApplicationTemplate(
136
- id: "${template.id}"
137
- ){
138
- id
139
- name
140
- description
141
- }
142
- }`
143
- return this._handleResponse(await this._request(query))
144
- }
145
-
146
53
  // Replace with fetch
147
54
  async _request(query, variables) {
148
55
  const opts = {
@@ -150,9 +57,7 @@ class UCLService extends cds.Service {
150
57
  path: this.options.path,
151
58
  agent: this.agent,
152
59
  method: 'POST',
153
- headers: {
154
- 'Content-Type': 'application/json'
155
- }
60
+ headers: { 'Content-Type': 'application/json' }
156
61
  }
157
62
  return new Promise((resolve, reject) => {
158
63
  const req = https.request(opts, res => {
@@ -186,27 +91,87 @@ class UCLService extends cds.Service {
186
91
  })
187
92
  }
188
93
 
94
+ _handleResponse(result) {
95
+ if (result.response && result.response.errors) {
96
+ let errorMessage = result.response.errors[0].message
97
+ throw new Error(errorMessage)
98
+ } else {
99
+ return result.result
100
+ }
101
+ }
102
+
103
+ async readTemplate() {
104
+ const xsappname = this.options.credentials.xsappname
105
+ const variables = { key: 'xsappname', value: `"${xsappname}"` }
106
+ const res = await this._request(READ_QUERY, variables)
107
+ if (res) return res.applicationTemplates.data[0]
108
+ }
109
+
110
+ async createTemplate() {
111
+ try {
112
+ return this._handleResponse(await this._request(CREATE_MUTATION, { input: this._applicationTemplate }))
113
+ } catch (e) {
114
+ this._handleResponse(e)
115
+ }
116
+ }
117
+
189
118
  async updateTemplate(template) {
190
- const query = `mutation {
191
- result: updateApplicationTemplate(
192
- id: "${template.id}"
193
- in: {
194
- name: "${this.options.systemType}"
195
- description: "${this.options.systemDescription}"
196
- applicationInput: {
197
- name: "${this.options.systemType}"
198
- description: "${this.options.systemDescription}"
199
- providerName: "${this.options.provider}"
200
- localTenantID: "{{tenant-id}}"
201
- labels: { displayName: "{{subdomain}}" }
119
+ try {
120
+ const input = { ...this._applicationTemplate }
121
+ delete input.labels
122
+ const response = this._handleResponse(await this._request(UPDATE_MUTATION, { id: template.id, input }))
123
+ LOG.info('Application template updated successfully.')
124
+ return response
125
+ } catch (e) {
126
+ this._handleResponse(e)
127
+ }
128
+ }
129
+
130
+ async deleteTemplate() {
131
+ const template = await this.readTemplate()
132
+ if (!template) return
133
+ return this._handleResponse(await this._request(DELETE_MUTATION, { id: template.id }))
134
+ }
135
+ }
136
+
137
+ const READ_QUERY = `
138
+ query ($key: String!, $value: String!) {
139
+ applicationTemplates(filter: { key: $key, query: $value }) {
140
+ data {
141
+ id
142
+ name
143
+ description
144
+ placeholders {
145
+ name
146
+ description
147
+ }
148
+ applicationInput
149
+ labels
150
+ webhooks {
151
+ type
202
152
  }
203
- applicationNamespace: "${this.options.namespace}"
204
- placeholders: [
205
- { name: "subdomain", description: "The subdomain of the consumer tenant" }
206
- { name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.subscribedTenantId" }
207
- ]
208
- accessLevel: GLOBAL
209
153
  }
154
+ }
155
+ }`
156
+
157
+ const CREATE_MUTATION = `
158
+ mutation {
159
+ result: createApplicationTemplate (
160
+ in: $input
161
+ ) {
162
+ id
163
+ name
164
+ labels
165
+ applicationInput
166
+ applicationNamespace
167
+ }
168
+ }`
169
+
170
+ const UPDATE_MUTATION = `
171
+ mutation ($id: ID!, $input: ApplicationTemplateUpdateInput!) {
172
+ result: updateApplicationTemplate(
173
+ id: $id
174
+ in: $input
210
175
  ) {
211
176
  id
212
177
  name
@@ -215,14 +180,58 @@ class UCLService extends cds.Service {
215
180
  applicationInput
216
181
  }
217
182
  }`
218
- try {
219
- const response = this._handleResponse(await this._request(query))
220
- LOG.info('Application template updated successfully.')
221
- return response
222
- } catch (e) {
223
- this._handleResponse(e)
183
+
184
+ const DELETE_MUTATION = `
185
+ mutation ($id: ID!) {
186
+ result: deleteApplicationTemplate(
187
+ id: $id
188
+ ) {
189
+ id
190
+ name
191
+ description
224
192
  }
193
+ }`
194
+
195
+ const _deepAssign = (a, b) => {
196
+ for (const key in b) {
197
+ if (typeof b[key] === 'object' && !Array.isArray(b[key])) a[key] = _deepAssign(a[key] || {}, b[key])
198
+ else a[key] = b[key]
225
199
  }
200
+ return a
201
+ }
202
+
203
+ const _getApplicationTemplate = options => {
204
+ let applicationTemplate = {
205
+ applicationInput: {
206
+ providerName: 'SAP',
207
+ localTenantID: '{{tenant-id}}',
208
+ labels: {
209
+ displayName: '{{subdomain}}'
210
+ }
211
+ },
212
+ labels: {
213
+ managed_app_provisioning: true,
214
+ xsappname: '${xsappname}'
215
+ },
216
+ placeholders: [
217
+ { name: 'subdomain', description: 'The subdomain of the consumer tenant' },
218
+ {
219
+ name: 'tenant-id',
220
+ description: "The tenant id as it's known in the product's domain",
221
+ jsonPath: '$.subscribedTenantId'
222
+ }
223
+ ],
224
+ accessLevel: 'GLOBAL'
225
+ }
226
+ applicationTemplate = _deepAssign(applicationTemplate, options.applicationTemplate)
227
+
228
+ const pkg = require(cds.root + '/package')
229
+ if (!applicationTemplate.name) applicationTemplate.name = pkg.name
230
+ if (!applicationTemplate.applicationInput.name) applicationTemplate.applicationInput.name = pkg.name
231
+ if (applicationTemplate.labels.xsappname === '${xsappname}')
232
+ applicationTemplate.labels.xsappname = options.credentials.xsappname
233
+
234
+ return applicationTemplate
226
235
  }
227
236
 
228
237
  module.exports = UCLService
@@ -1,13 +1,22 @@
1
+ const cds = require('../../../')
2
+
1
3
  const { where2obj } = require('../../_runtime/common/utils/cqn')
2
- const { getKeysForNavigationFromRefPath } = require('../../_runtime/common/utils/keys')
4
+ const propagateForeignKeys = require('../../_runtime/common/utils/propagateForeignKeys')
5
+
6
+ let _consistent_params
3
7
 
4
8
  // REVISIT: do we already have something like this _without using okra api_?
5
9
  // REVISIT: should we still support process.env.CDS_FEATURES_PARAMS? probably nobody uses it...
6
10
  exports.getKeysAndParamsFromPath = (from, { model }) => {
7
11
  if (!from.ref || !from.ref.length) return {}
8
12
 
9
- const keys = {}
13
+ // REVISIT: make opt-out feature in cds^9
14
+ _consistent_params ??= cds.env.features.consistent_params
15
+
10
16
  const params = []
17
+ const data = {}
18
+ let currData = data
19
+ const navigations = []
11
20
 
12
21
  let cur = model.definitions
13
22
  let lastElement
@@ -19,28 +28,31 @@ exports.getKeysAndParamsFromPath = (from, { model }) => {
19
28
  const target = cur[id]._target ?? lastElement
20
29
  cur = target.elements
21
30
 
22
- if (i === from.ref.length - 1) {
23
- // last element
24
- if (ref.where) {
25
- const seg_keys = where2obj(ref.where)
26
- Object.assign(keys, seg_keys)
27
- params[i] = seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys
28
- } else if (ref.args) {
29
- params[i] = Object.fromEntries(Object.entries(ref.args).map(([k, v]) => [k, 'val' in v ? v.val : v]))
30
- }
31
- if (lastElement.isAssociation && from.ref.length > 1) {
32
- // add keys for navigation from path
33
- const rootRef = from.ref[0]
34
- const rootTarget = model.definitions[rootRef.id || rootRef]
35
- const seg_keys = getKeysForNavigationFromRefPath(from.ref, rootTarget)
36
- // only take if a known property
37
- for (const k in seg_keys) if (k in cur) keys[k] = seg_keys[k]
38
- }
39
- } else if (ref.where) {
31
+ if (lastElement.isAssociation) {
32
+ currData[lastElement.name] = {}
33
+ currData = currData[lastElement.name]
34
+ navigations.push(lastElement)
35
+ }
36
+
37
+ if (ref.where) {
40
38
  const seg_keys = where2obj(ref.where)
41
- params[i] = seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys
39
+ if (_consistent_params) params[i] = seg_keys
40
+ else params[i] = seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys
41
+ Object.assign(currData, seg_keys)
42
42
  }
43
+
44
+ if (i === from.ref.length - 1 && !ref.where && ref.args) {
45
+ const seg_keys = Object.fromEntries(Object.entries(ref.args).map(([k, v]) => [k, 'val' in v ? v.val : v]))
46
+ if (_consistent_params) params[i] = seg_keys
47
+ else params[i] = seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys
48
+ }
49
+ }
50
+
51
+ let current = data
52
+ for (let nav of navigations) {
53
+ propagateForeignKeys(nav.name, current, nav._foreignKeys, true, { enumerable: true, generateKeys: false })
54
+ current = current[nav.name]
43
55
  }
44
56
 
45
- return { keys, params }
57
+ return { keys: current || {}, params }
46
58
  }
@@ -7,6 +7,7 @@ const postProcess = require('../utils/postProcess')
7
7
  const readAfterWrite4 = require('../utils/readAfterWrite')
8
8
  const getODataResult = require('../utils/result')
9
9
  const normalizeTimeData = require('../utils/normalizeTimeData')
10
+ const odataBind = require('../utils/odataBind')
10
11
 
11
12
  const { getKeysAndParamsFromPath } = require('../../common/utils/path')
12
13
 
@@ -37,6 +38,7 @@ module.exports = (adapter, isUpsert) => {
37
38
  const msg = 'Only single entity representations are allowed'
38
39
  throw Object.assign(new Error(msg), { statusCode: 400 })
39
40
  }
41
+ odataBind(data, target)
40
42
  normalizeTimeData(data, model, target)
41
43
  const { keys, params } = getKeysAndParamsFromPath(from, { model })
42
44
  // add keys from url into payload (overwriting if already present)
@@ -113,7 +113,10 @@ module.exports = adapter => {
113
113
  handleSapMessages(cdsReq, req, res)
114
114
 
115
115
  const stream = getReadable(result)
116
- if (!stream) return res.sendStatus(204)
116
+ if (!stream) {
117
+ if (res.statusCode > 200) return res.end()
118
+ return res.sendStatus(204)
119
+ }
117
120
 
118
121
  const { mimetype, filename, disposition } = collectStreamMetadata(result, operation, query)
119
122
  validateMimetypeIsAcceptedOrThrow(req.headers, mimetype)
@@ -151,7 +154,10 @@ module.exports = adapter => {
151
154
  handleSapMessages(cdsReq, req, res)
152
155
 
153
156
  if (operation.returns?.items && result == null) result = []
154
- if (!operation.returns || result == null) return res.sendStatus(204)
157
+ if (!operation.returns || result == null) {
158
+ if (res.statusCode > 200) return res.end()
159
+ return res.sendStatus(204)
160
+ }
155
161
 
156
162
  if (operation.returns._type?.match?.(/^cds\./)) {
157
163
  const context = `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`
@@ -9,7 +9,7 @@ module.exports = adapter => {
9
9
 
10
10
  if (req._query) return next() //> already parsed (e.g., upsert)
11
11
 
12
- req._query = cds.odata.parse(req.url, { service, baseUrl: req.baseUrl, strict: true })
12
+ req._query = cds.odata.parse(req.url, { service, baseUrl: req.baseUrl, strict: true, protocol: 'odata' })
13
13
 
14
14
  next()
15
15
  }
@@ -9,7 +9,6 @@ const {
9
9
  validateMimetypeIsAcceptedOrThrow,
10
10
  getReadable
11
11
  } = require('../../common/utils/streaming')
12
- const { getTransition } = require('../../_runtime/common/utils/resolveView')
13
12
 
14
13
  const _resolveContentProperty = (target, annotName, resolvedProp) => {
15
14
  if (target.elements[resolvedProp]) {
@@ -19,7 +18,7 @@ const _resolveContentProperty = (target, annotName, resolvedProp) => {
19
18
  LOG.warn(
20
19
  `"${annotName}" in entity "${target.name}" points to property "${resolvedProp}" which was renamed or is not part of the projection. You must update the annotation value.`
21
20
  )
22
- const mapping = getTransition(target, cds.db).mapping
21
+ const mapping = cds.ql.resolve.transitions({ target }, cds.db).mapping
23
22
  const key = [...mapping.entries()].find(({ 1: val }) => val.ref[0] === resolvedProp)
24
23
  return key?.length && key[0]
25
24
  }
@@ -7,6 +7,7 @@ const postProcess = require('../utils/postProcess')
7
7
  const readAfterWrite4 = require('../utils/readAfterWrite')
8
8
  const getODataResult = require('../utils/result')
9
9
  const normalizeTimeData = require('../utils/normalizeTimeData')
10
+ const odataBind = require('../utils/odataBind')
10
11
 
11
12
  const { getKeysAndParamsFromPath } = require('../../common/utils/path')
12
13
 
@@ -70,6 +71,7 @@ module.exports = adapter => {
70
71
 
71
72
  // payload & params
72
73
  const data = _propertyAccess ? { [_propertyAccess]: req.body.value } : req.body
74
+ odataBind(data, target)
73
75
  normalizeTimeData(data, model, target)
74
76
  const { keys, params } = getKeysAndParamsFromPath(from, { model })
75
77
  // add keys from url into payload (overwriting if already present)
@@ -241,7 +241,7 @@ function _handleCollectionBoundActions(current, ref, i, namespace, one) {
241
241
  action = shortName && current.actions[shortName]
242
242
  }
243
243
 
244
- let incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
244
+ let incompleteKeys = !(!!ref[i].where || i === ref.length - 1 || one)
245
245
  if (!action) return incompleteKeys
246
246
 
247
247
  const onCollection = !!(
@@ -301,7 +301,9 @@ function _processSegments(from, model, namespace, cqn, protocol) {
301
301
 
302
302
  if (incompleteKeys) {
303
303
  // > key
304
- keys = keys || keysOf(current, protocol !== 'rest') // if odata, skip backlinks as key as they are used from structure
304
+ // in case of odata, values for keys that are backlinks are expected to be omitted
305
+ keys = keys || keysOf(current, !!protocol?.match(/odata/i))
306
+ if (!keys.length) cds.error(`Invalid resource path "${path}"`, { code: '404', statusCode: 404 })
305
307
  let key = keys[keyCount++]
306
308
  one = true
307
309
  const element = current.elements[key]
@@ -388,6 +390,11 @@ function _processSegments(from, model, namespace, cqn, protocol) {
388
390
  if (!Object.keys(params).length) params = where2obj(ref[i].where)
389
391
  _processWhere(ref[i].where, current)
390
392
  _checkAllKeysProvided(params, current)
393
+
394
+ if (keyCount === 0 && !Object.keys(params).length && whereRef.length === 1) {
395
+ const msg = `Entity "${current.name}" can not be accessed by key.`
396
+ throw Object.assign(new Error(msg), { statusCode: 400 })
397
+ }
391
398
  }
392
399
  } else if ({ action: 1, function: 1 }[current.kind]) {
393
400
  // > action or function
@@ -578,8 +585,6 @@ function _processColumns(cqn, target, protocol) {
578
585
 
579
586
  let columns = cqn.SELECT.columns
580
587
 
581
- // REVISIT Keys should be added only in case of odata, not e.g. rest.
582
- // Currently odata is detected via odata_new_parser flag -> find a better indicator.
583
588
  if (columns && !cqn.SELECT.groupBy) {
584
589
  let entity
585
590
  if (target.kind === 'entity') entity = target
@@ -588,7 +593,9 @@ function _processColumns(cqn, target, protocol) {
588
593
 
589
594
  _removeUnneededColumnsIfHasAsterisk(columns)
590
595
  rewriteExpandAsterisk(columns, entity)
591
- if (protocol !== 'rest') _addKeys(columns, entity)
596
+
597
+ // in case of odata, add all missing key fields (i.e., not in $select)
598
+ if (protocol?.match(/odata/i)) _addKeys(columns, entity)
592
599
  }
593
600
 
594
601
  if (!Array.isArray(columns)) return
@@ -755,6 +762,8 @@ function _validateQuery(SELECT, target, isOne, model) {
755
762
  }
756
763
 
757
764
  module.exports = (cqn, model, namespace, protocol) => {
765
+ if (!model) return cqn
766
+
758
767
  const from = resolveFromSelect(cqn)
759
768
  const { ref } = from
760
769
 
@@ -816,11 +825,10 @@ module.exports = (cqn, model, namespace, protocol) => {
816
825
  break
817
826
  }
818
827
  }
819
- const setRecurseRef = SELECT => {
820
- if (SELECT.from.SELECT) setRecurseRef(SELECT.from.SELECT)
821
- if (SELECT.recurse) SELECT.recurse.ref[0] = uplinkName
828
+ if (uplinkName) {
829
+ let r = cqn.SELECT.recurse
830
+ if (r) r.ref[0] = uplinkName
822
831
  }
823
- if (uplinkName) setRecurseRef(cqn.SELECT)
824
832
  }
825
833
 
826
834
  // REVISIT: better