@sap/cds 9.2.1 → 9.3.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 (72) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/_i18n/i18n_es.properties +3 -3
  3. package/_i18n/i18n_es_MX.properties +3 -3
  4. package/_i18n/i18n_fr.properties +2 -2
  5. package/_i18n/messages.properties +6 -0
  6. package/app/index.js +0 -1
  7. package/bin/deploy.js +1 -1
  8. package/bin/serve.js +7 -20
  9. package/lib/compile/cdsc.js +3 -0
  10. package/lib/compile/for/flows.js +102 -0
  11. package/lib/compile/for/nodejs.js +28 -0
  12. package/lib/compile/to/edm.js +11 -4
  13. package/lib/core/classes.js +1 -1
  14. package/lib/core/linked-csn.js +8 -0
  15. package/lib/dbs/cds-deploy.js +12 -12
  16. package/lib/env/cds-env.js +1 -1
  17. package/lib/env/cds-requires.js +21 -20
  18. package/lib/env/defaults.js +2 -1
  19. package/lib/index.js +5 -6
  20. package/lib/log/cds-log.js +6 -5
  21. package/lib/log/format/aspects/cf.js +2 -2
  22. package/lib/plugins.js +1 -1
  23. package/lib/ql/cds-ql.js +0 -3
  24. package/lib/req/request.js +3 -3
  25. package/lib/req/response.js +12 -7
  26. package/lib/srv/bindings.js +17 -17
  27. package/lib/srv/cds-connect.js +6 -9
  28. package/lib/srv/cds-serve.js +74 -137
  29. package/lib/srv/cds.Service.js +49 -0
  30. package/lib/srv/factory.js +4 -4
  31. package/lib/srv/middlewares/auth/ias-auth.js +33 -9
  32. package/lib/srv/middlewares/auth/index.js +3 -2
  33. package/lib/srv/middlewares/auth/jwt-auth.js +20 -6
  34. package/lib/srv/protocols/hcql.js +16 -1
  35. package/lib/srv/srv-dispatch.js +1 -1
  36. package/lib/utils/cds-utils.js +4 -8
  37. package/lib/utils/csv-reader.js +27 -7
  38. package/libx/_runtime/cds.js +0 -6
  39. package/libx/_runtime/common/Service.js +5 -0
  40. package/libx/_runtime/common/generic/crud.js +1 -1
  41. package/libx/_runtime/common/generic/flows.js +106 -0
  42. package/libx/_runtime/common/generic/paging.js +3 -3
  43. package/libx/_runtime/common/utils/differ.js +5 -15
  44. package/libx/_runtime/common/utils/resolveView.js +2 -2
  45. package/libx/_runtime/common/utils/rewriteAsterisks.js +10 -4
  46. package/libx/_runtime/fiori/lean-draft.js +76 -40
  47. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  48. package/libx/_runtime/messaging/service.js +7 -0
  49. package/libx/_runtime/remote/Service.js +68 -62
  50. package/libx/_runtime/remote/utils/client.js +29 -216
  51. package/libx/_runtime/remote/utils/query.js +197 -0
  52. package/libx/_runtime/ucl/Service.js +180 -112
  53. package/libx/_runtime/ucl/queries.js +61 -0
  54. package/libx/odata/ODataAdapter.js +1 -4
  55. package/libx/odata/index.js +2 -10
  56. package/libx/odata/middleware/error.js +8 -1
  57. package/libx/odata/middleware/stream.js +1 -1
  58. package/libx/odata/middleware/update.js +12 -2
  59. package/libx/odata/parse/afterburner.js +113 -20
  60. package/libx/odata/parse/cqn2odata.js +1 -3
  61. package/libx/rest/middleware/parse.js +9 -2
  62. package/package.json +2 -2
  63. package/server.js +2 -0
  64. package/srv/app-service.js +1 -0
  65. package/srv/db-service.js +1 -0
  66. package/srv/msg-service.js +1 -0
  67. package/srv/remote-service.js +1 -0
  68. package/srv/ucl-service.cds +32 -0
  69. package/srv/ucl-service.js +1 -0
  70. package/lib/ql/resolve.js +0 -45
  71. package/libx/common/assert/type-strict.js +0 -109
  72. package/libx/common/assert/utils.js +0 -60
@@ -4,11 +4,163 @@ const LOG = cds.log('ucl')
4
4
  const fs = require('fs').promises
5
5
  const https = require('https')
6
6
 
7
- class UCLService extends cds.Service {
7
+ const { READ_QUERY, CREATE_MUTATION, UPDATE_MUTATION, DELETE_MUTATION } = require('./queries')
8
+ const TRUSTED_CERT = {
9
+ CANARY: {
10
+ ISSUER: 'CN=SAP PKI Certificate Service Client CA,OU=SAP BTP Clients,O=SAP SE,L=cf-eu10-canary,C=DE',
11
+ SUBJECT: 'CN=cmp-stage,OU=SAP Cloud Platform Clients,OU=Canary,OU=cmp-cf-eu10-canary,O=SAP SE,L=Stage,C=DE'
12
+ },
13
+ LIVE: {
14
+ ISSUER: 'CN=SAP PKI Certificate Service Client CA,OU=SAP BTP Clients,O=SAP SE,L=cf-eu10,C=DE',
15
+ SUBJECT: 'CN=cmp-prod,OU=SAP Cloud Platform Clients,OU=cmp-cf-eu10,O=SAP SE,L=Prod,C=DE'
16
+ }
17
+ }
18
+
19
+ module.exports = class UCLService extends cds.Service {
20
+ constructor(...args) {
21
+ super(...args)
22
+
23
+ // REVISIT: cds.connect.to('ucl') should return this, but no cds.requires.kinds.ucl config achieved this
24
+ cds.services.ucl = this
25
+ }
26
+
8
27
  async init() {
28
+ this.on('*', 'tenantMappings', async req => {
29
+ if (req.method !== 'PATCH') req.reject(405, `Method ${req.method} not allowed for tenant mapping notifications`)
30
+
31
+ await this._validateCertificate(req)
32
+
33
+ return await this._dispatchNotification(req)
34
+ })
35
+
9
36
  await super.init()
10
37
 
11
- this._applicationTemplate = _getApplicationTemplate(this.options)
38
+ if (cds.env.requires.ucl?.applicationTemplate) await this._upsertApplicationTemplate(cds.env.requires.ucl)
39
+ }
40
+
41
+ async _validateCertificate(req) {
42
+ // Allow bypassing certificate validation in development
43
+ if (process.env.NODE_ENV !== 'production' && cds.env.requires.ucl?.skipCertValidation) return
44
+
45
+ // Check if the request uses a 'cert' or 'mesh.cf' domain
46
+ if (!req.http?.req.hostname.match(/\.cert\.|\.mesh\.cf\./)) req.reject(403)
47
+
48
+ // Verify presence of required .cert domain headers
49
+ const reqClientVerificationStatus = req.headers['x-ssl-client-verify']
50
+ if (reqClientVerificationStatus !== '0') throw new cds.error(401, 'Client certificate not verified')
51
+ const reqClientCertSubjectB64 = req.headers['x-ssl-client-subject-dn']
52
+ if (!reqClientCertSubjectB64) throw new cds.error(401, 'No client certificate subject provided')
53
+ const reqClientCertIssuerB64 = req.headers['x-ssl-client-issuer-dn']
54
+ if (!reqClientCertIssuerB64) throw new cds.error(401, 'No client certificate issuer provided')
55
+
56
+ // Extract tokens from base64 encoded subject and issuer .cert domain headers
57
+ const reqClientCertSubject = Buffer.from(reqClientCertSubjectB64, 'base64').toString('ascii')
58
+ const reqClientCertSubjectTokens = reqClientCertSubject
59
+ .replace('\n', '')
60
+ .split('/')
61
+ .filter(token => token)
62
+ const reqClientCertIssuer = Buffer.from(reqClientCertIssuerB64, 'base64').toString('ascii')
63
+ const reqClientCertIssuerTokens = reqClientCertIssuer
64
+ .replace('\n', '')
65
+ .split('/')
66
+ .filter(token => token)
67
+
68
+ // Determine trusted certificate subject and issuer information
69
+ let trustedCertSubject = process.env.CDS_UCL_X509_CERTSUBJECT || cds.env.requires.ucl.x509?.certSubject
70
+ let trustedCertIssuer = process.env.CDS_UCL_X509_CERTISSUER || cds.env.requires.ucl.x509?.certIssuer
71
+ if (!trustedCertIssuer?.length || !trustedCertSubject?.length) {
72
+ let stage = 'CANARY'
73
+ const VCAP_APPLICATION = JSON.parse(process.env.VCAP_APPLICATION || '{}')
74
+ if (VCAP_APPLICATION.cf_api && !VCAP_APPLICATION.cf_api.endsWith('.cf.sap.hana.ondemand.com')) stage = 'LIVE'
75
+ trustedCertIssuer = TRUSTED_CERT[stage].ISSUER
76
+ trustedCertSubject = TRUSTED_CERT[stage].SUBJECT
77
+ }
78
+
79
+ // Match received with trusted info - As done in UCL reference implementation
80
+ const matchesUclInfoSubject = trustedCertSubject
81
+ .split(',')
82
+ .map(token => token.trim())
83
+ .every(token => reqClientCertSubjectTokens.includes(token))
84
+ if (!matchesUclInfoSubject) {
85
+ LOG.debug('Received Request Subject Info: ', reqClientCertSubject)
86
+ LOG.debug('Expected UCL Subject Info: ', trustedCertSubject)
87
+ throw new cds.error(401, 'Received .cert subject does not match trusted UCL info subject')
88
+ }
89
+ const matchesUclInfoIssuer = trustedCertIssuer
90
+ .split(',')
91
+ .map(token => token.trim())
92
+ .every(token => reqClientCertIssuerTokens.includes(token))
93
+ if (!matchesUclInfoIssuer) {
94
+ LOG.debug('Received Request Issuer Info: ', reqClientCertIssuer)
95
+ LOG.debug('Expected UCL Issuer Info: ', trustedCertIssuer)
96
+ throw new cds.error(401, 'Received .cert issuer does not match trusted UCL info issuer')
97
+ }
98
+ }
99
+
100
+ async _dispatchNotification(req) {
101
+ // Call User defined handlers for tenant mapping notifications
102
+
103
+ // REVISIT: Java unpacks the location header and enables the async flow aswell
104
+ if (req.headers.location)
105
+ throw new cds.error(400, 'Location header found in tenant mapping notification: Async flow not supported!')
106
+
107
+ LOG.debug('Tenant mapping notification: ', req.data)
108
+
109
+ const { operation } = req.data?.context ?? {}
110
+ if (operation !== 'assign' && operation !== 'unassign')
111
+ throw new cds.error(
112
+ 400,
113
+ `Invalid operation "${operation}" in tenant mapping notification. Expected "assign" or "unassign".`
114
+ )
115
+
116
+ const response = (await this.send(operation, req.data)) ?? {}
117
+
118
+ if (response.error) req.http.res.status(400)
119
+ else response.state ??= operation === 'assign' ? 'CONFIG_PENDING' : 'READY'
120
+
121
+ return response
122
+ }
123
+
124
+ /*
125
+ * the rest is for upserting the application template
126
+ */
127
+
128
+ async _upsertApplicationTemplate() {
129
+ const _getApplicationTemplate = options => {
130
+ let applicationTemplate = {
131
+ applicationInput: {
132
+ providerName: 'SAP',
133
+ localTenantID: '{{tenant-id}}',
134
+ labels: {
135
+ displayName: '{{subdomain}}'
136
+ }
137
+ },
138
+ labels: {
139
+ managed_app_provisioning: true,
140
+ xsappname: '${xsappname}'
141
+ },
142
+ placeholders: [
143
+ { name: 'subdomain', description: 'The subdomain of the consumer tenant' },
144
+ {
145
+ name: 'tenant-id',
146
+ description: "The tenant id as it's known in the product's domain",
147
+ jsonPath: '$.subscribedTenantId'
148
+ }
149
+ ],
150
+ accessLevel: 'GLOBAL'
151
+ }
152
+ applicationTemplate = cds.utils.merge(applicationTemplate, options.applicationTemplate)
153
+
154
+ const pkg = require(cds.root + '/package')
155
+ if (!applicationTemplate.name) applicationTemplate.name = pkg.name
156
+ if (!applicationTemplate.applicationInput.name) applicationTemplate.applicationInput.name = pkg.name
157
+ if (applicationTemplate.labels.xsappname === '${xsappname}')
158
+ applicationTemplate.labels.xsappname = options.credentials.xsappname
159
+
160
+ return applicationTemplate
161
+ }
162
+
163
+ this._applicationTemplate = _getApplicationTemplate(cds.env.requires.ucl)
12
164
  if (!this._applicationTemplate.applicationNamespace) {
13
165
  throw new Error(
14
166
  'The UCL service requires a valid `applicationTemplate`, please provide it as described in the documentation.'
@@ -19,22 +171,25 @@ class UCLService extends cds.Service {
19
171
  throw new Error(
20
172
  'The UCL service requires multitenancy, please enable it in your cds configuration with `cds.requires.multitenancy` or by using the mtx sidecar.'
21
173
  )
22
- if (!this.options.credentials)
174
+ if (!cds.env.requires.ucl.credentials)
23
175
  throw new Error('No credentials found for the UCL service, please bind the service to your app.')
24
176
 
25
- if (!this.options.x509.cert && !this.options.x509.certPath)
177
+ if (!cds.env.requires.ucl.x509?.cert && !cds.env.requires.ucl.x509?.certPath)
26
178
  throw new Error('UCL requires `x509.cert` or `x509.certPath`.')
27
- if (!this.options.x509.pkey && !this.options.x509.pkeyPath)
179
+ if (!cds.env.requires.ucl.x509?.pkey && !cds.env.requires.ucl.x509?.pkeyPath)
28
180
  throw new Error('UCL requires `x509.pkey` or `x509.pkeyPath`.')
29
181
 
30
182
  const [cert, key] = await Promise.all([
31
- this.options.x509.cert ?? fs.readFile(cds.utils.path.resolve(cds.root, this.options.x509.certPath)),
32
- this.options.x509.pkey ?? fs.readFile(cds.utils.path.resolve(cds.root, this.options.x509.pkeyPath))
183
+ cds.env.requires.ucl.x509?.cert ??
184
+ fs.readFile(cds.utils.path.resolve(cds.root, cds.env.requires.ucl.x509?.certPath)),
185
+ cds.env.requires.ucl.x509?.pkey ??
186
+ fs.readFile(cds.utils.path.resolve(cds.root, cds.env.requires.ucl.x509?.pkeyPath))
33
187
  ])
188
+
34
189
  this.agent = new https.Agent({ cert, key })
35
190
 
36
- const existingTemplate = await this.readTemplate()
37
- const template = existingTemplate ? await this.updateTemplate(existingTemplate) : await this.createTemplate() // TODO: Make sure return value is correct
191
+ const existingTemplate = await this._readTemplate()
192
+ const template = existingTemplate ? await this._updateTemplate(existingTemplate) : await this._createTemplate() // TODO: Make sure return value is correct
38
193
 
39
194
  if (!template) throw new Error('The UCL service could not create an application template.')
40
195
 
@@ -50,15 +205,18 @@ class UCLService extends cds.Service {
50
205
  })
51
206
  }
52
207
 
53
- // Replace with fetch
208
+ // REVISIT: Replace with fetch (?)
54
209
  async _request(query, variables) {
210
+ // Query GraphQL API
211
+
55
212
  const opts = {
56
- host: this.options.host,
57
- path: this.options.path,
213
+ host: cds.env.requires.ucl.host || 'compass-gateway-sap-mtls.mps.kyma.cloud.sap',
214
+ path: cds.env.requires.ucl.path || '/director/graphql',
58
215
  agent: this.agent,
59
216
  method: 'POST',
60
217
  headers: { 'Content-Type': 'application/json' }
61
218
  }
219
+
62
220
  return new Promise((resolve, reject) => {
63
221
  const req = https.request(opts, res => {
64
222
  const chunks = []
@@ -73,9 +231,12 @@ class UCLService extends cds.Service {
73
231
  headers: res.headers,
74
232
  body: Buffer.concat(chunks).toString()
75
233
  }
234
+
76
235
  const body = JSON.parse(response.body)
236
+
77
237
  if (body.errors)
78
238
  throw new Error('Request to UCL service failed with:\n' + JSON.stringify(body.errors, null, 2))
239
+
79
240
  resolve(body.data)
80
241
  })
81
242
  })
@@ -87,6 +248,7 @@ class UCLService extends cds.Service {
87
248
  if (query) {
88
249
  req.write(JSON.stringify({ query, variables }))
89
250
  }
251
+
90
252
  req.end()
91
253
  })
92
254
  }
@@ -100,14 +262,14 @@ class UCLService extends cds.Service {
100
262
  }
101
263
  }
102
264
 
103
- async readTemplate() {
104
- const xsappname = this.options.credentials.xsappname
265
+ async _readTemplate() {
266
+ const xsappname = cds.env.requires.ucl.credentials.xsappname
105
267
  const variables = { key: 'xsappname', value: `"${xsappname}"` }
106
268
  const res = await this._request(READ_QUERY, variables)
107
269
  if (res) return res.applicationTemplates.data[0]
108
270
  }
109
271
 
110
- async createTemplate() {
272
+ async _createTemplate() {
111
273
  try {
112
274
  return this._handleResponse(await this._request(CREATE_MUTATION, { input: this._applicationTemplate }))
113
275
  } catch (e) {
@@ -115,7 +277,7 @@ class UCLService extends cds.Service {
115
277
  }
116
278
  }
117
279
 
118
- async updateTemplate(template) {
280
+ async _updateTemplate(template) {
119
281
  try {
120
282
  const input = { ...this._applicationTemplate }
121
283
  delete input.labels
@@ -127,103 +289,9 @@ class UCLService extends cds.Service {
127
289
  }
128
290
  }
129
291
 
130
- async deleteTemplate() {
131
- const template = await this.readTemplate()
292
+ async _deleteTemplate() {
293
+ const template = await this._readTemplate()
132
294
  if (!template) return
133
295
  return this._handleResponse(await this._request(DELETE_MUTATION, { id: template.id }))
134
296
  }
135
297
  }
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
152
- }
153
- }
154
- }
155
- }`
156
-
157
- const CREATE_MUTATION = `
158
- mutation ($input: ApplicationTemplateInput!) {
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
175
- ) {
176
- id
177
- name
178
- labels
179
- description
180
- applicationInput
181
- }
182
- }`
183
-
184
- const DELETE_MUTATION = `
185
- mutation ($id: ID!) {
186
- result: deleteApplicationTemplate(
187
- id: $id
188
- ) {
189
- id
190
- name
191
- description
192
- }
193
- }`
194
-
195
- const _getApplicationTemplate = options => {
196
- let applicationTemplate = {
197
- applicationInput: {
198
- providerName: 'SAP',
199
- localTenantID: '{{tenant-id}}',
200
- labels: {
201
- displayName: '{{subdomain}}'
202
- }
203
- },
204
- labels: {
205
- managed_app_provisioning: true,
206
- xsappname: '${xsappname}'
207
- },
208
- placeholders: [
209
- { name: 'subdomain', description: 'The subdomain of the consumer tenant' },
210
- {
211
- name: 'tenant-id',
212
- description: "The tenant id as it's known in the product's domain",
213
- jsonPath: '$.subscribedTenantId'
214
- }
215
- ],
216
- accessLevel: 'GLOBAL'
217
- }
218
- applicationTemplate = cds.utils.merge(applicationTemplate, options.applicationTemplate)
219
-
220
- const pkg = require(cds.root + '/package')
221
- if (!applicationTemplate.name) applicationTemplate.name = pkg.name
222
- if (!applicationTemplate.applicationInput.name) applicationTemplate.applicationInput.name = pkg.name
223
- if (applicationTemplate.labels.xsappname === '${xsappname}')
224
- applicationTemplate.labels.xsappname = options.credentials.xsappname
225
-
226
- return applicationTemplate
227
- }
228
-
229
- module.exports = UCLService
@@ -0,0 +1,61 @@
1
+ const READ_QUERY = /* GraphQL */ `
2
+ query ($key: String!, $value: String!) {
3
+ applicationTemplates(filter: { key: $key, query: $value }) {
4
+ data {
5
+ id
6
+ name
7
+ description
8
+ placeholders {
9
+ name
10
+ description
11
+ }
12
+ applicationInput
13
+ labels
14
+ webhooks {
15
+ type
16
+ }
17
+ }
18
+ }
19
+ }
20
+ `
21
+
22
+ const CREATE_MUTATION = /* GraphQL */ `
23
+ mutation ($input: ApplicationTemplateInput!) {
24
+ result: createApplicationTemplate(in: $input) {
25
+ id
26
+ name
27
+ labels
28
+ applicationInput
29
+ applicationNamespace
30
+ }
31
+ }
32
+ `
33
+
34
+ const UPDATE_MUTATION = /* GraphQL */ `
35
+ mutation ($id: ID!, $input: ApplicationTemplateUpdateInput!) {
36
+ result: updateApplicationTemplate(id: $id, in: $input) {
37
+ id
38
+ name
39
+ labels
40
+ description
41
+ applicationInput
42
+ }
43
+ }
44
+ `
45
+
46
+ const DELETE_MUTATION = /* GraphQL */ `
47
+ mutation ($id: ID!) {
48
+ result: deleteApplicationTemplate(id: $id) {
49
+ id
50
+ name
51
+ description
52
+ }
53
+ }
54
+ `
55
+
56
+ module.exports = {
57
+ READ_QUERY,
58
+ CREATE_MUTATION,
59
+ UPDATE_MUTATION,
60
+ DELETE_MUTATION
61
+ }
@@ -21,9 +21,6 @@ const error4 = require('./middleware/error')
21
21
 
22
22
  const { isStream } = require('./utils')
23
23
 
24
- // REVISIT: copied from lib/req/request.js
25
- const Http2Crud = { POST: 'CREATE', GET: 'READ', PUT: 'UPDATE', PATCH: 'UPDATE', DELETE: 'DELETE' }
26
-
27
24
  module.exports = class ODataAdapter extends HttpAdapter {
28
25
  request4(args) {
29
26
  return new ODataRequest(args)
@@ -105,7 +102,7 @@ module.exports = class ODataAdapter extends HttpAdapter {
105
102
 
106
103
  if (req._subrequest) {
107
104
  //> req._subrequest is set for batch subrequests
108
- LOG._info && LOG.info('>', Http2Crud[req.method], req.path, Object.keys(req.query).length ? { ...req.query } : '')
105
+ LOG._info && LOG.info('>', req.method, req.path, Object.keys(req.query).length ? { ...req.query } : '')
109
106
  } else {
110
107
  super.log(req)
111
108
  }
@@ -3,8 +3,8 @@
3
3
  const cds = require('../..')
4
4
  const { decodeURIComponent } = cds.utils
5
5
 
6
- const odata2cqn = require('./parse/parser').parse
7
- const cqn2odata = require('./parse/cqn2odata')
6
+ const { parse: odata2cqn } = require('./parse/parser')
7
+ const { cqn2odata } = require('./parse/cqn2odata')
8
8
 
9
9
  const afterburner = require('./parse/afterburner')
10
10
  const { getSafeNumber: safeNumber, skipToken } = require('./utils')
@@ -95,14 +95,6 @@ module.exports = {
95
95
 
96
96
  url = decodeURIComponent(url)
97
97
 
98
- // REVISIT: compat for bad url in mtxs tests (cf. #957)
99
- if (url.match(/\?\?/)) {
100
- const split = url.split('?')
101
- url = split.shift() + '?'
102
- while (split[0] === '') split.shift()
103
- url += split.join('?')
104
- }
105
-
106
98
  options = options === 'strict' ? { strict } : options.strict ? { ...options, strict } : options
107
99
  if (options.service?.model) Object.assign(options, { minimal: true, afterburner })
108
100
  options.safeNumber = safeNumber
@@ -36,7 +36,13 @@ exports.getSapMessages = (messages, req) => {
36
36
 
37
37
  const { i18n } = require('../../../lib')
38
38
  const ODATA_PROPERTIES = { code: 1, message: 1, target: 1, details: 1, innererror: 1 }
39
- const SAP_MSG_PROPERTIES = { ...ODATA_PROPERTIES, longtextUrl: 2, transition: 2, numericSeverity: 2 }
39
+ const SAP_MSG_PROPERTIES = {
40
+ ...ODATA_PROPERTIES,
41
+ longtextUrl: 2,
42
+ transition: 2,
43
+ numericSeverity: 2,
44
+ additionalTargets: 2
45
+ }
40
46
  const BAD_REQUESTS = { ENTITY_ALREADY_EXISTS: 1, FK_CONSTRAINT_VIOLATION: 2, UNIQUE_CONSTRAINT_VIOLATION: 3 }
41
47
 
42
48
  // prettier-ignore
@@ -64,6 +70,7 @@ const _normalize = (err, req, keep,
64
70
  if (keep) for (let k in this) if (k in keep || k[0] === '@') that[k] = this[k]
65
71
  if (req._is_odata && keep !== SAP_MSG_PROPERTIES) {
66
72
  that['@Common.numericSeverity'] ??= err.numericSeverity || 4
73
+ if (this.additionalTargets) that['@Common.additionalTargets'] ??= this.additionalTargets
67
74
  if (content_id) that['@Core.ContentID'] = content_id
68
75
  }
69
76
  if (locale) that.message = _message4 (err, key, locale)
@@ -19,7 +19,7 @@ const _resolveContentProperty = (target, annotName, resolvedProp) => {
19
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.`
20
20
  )
21
21
  // REVISIT: do not allow renaming of content type property. always rely on compiler resolving.
22
- const mapping = cds.ql.resolve.transitions({ _target: target }, cds.db).mapping
22
+ const mapping = cds.db.resolve.transitions({ _target: target }).mapping
23
23
  const key = [...mapping.entries()].find(({ 1: val }) => val.ref[0] === resolvedProp)
24
24
  return key?.length && key[0]
25
25
  }
@@ -53,13 +53,23 @@ module.exports = adapter => {
53
53
 
54
54
  // REVISIT: patch on collection is allowed in odata 4.01
55
55
  if (!one) {
56
- throw Object.assign(new Error(`Method ${req.method} is not allowed for entity collections`), { statusCode: 405 })
56
+ if (req.method === 'PATCH')
57
+ throw cds.error(`Method ${req.method} is not allowed for entity collections`, { status: 405 })
58
+ const entity = service.model?.definitions[req._query._subject.ref[0]]
59
+ const keys = {},
60
+ data = req.body || {}
61
+ for (let k in entity.keys)
62
+ keys[k] =
63
+ data[k] ||
64
+ (entity.keys[k]['@cds.on.insert']?.['='] === '$user' && cds.context?.user?.id) ||
65
+ cds.error(`All keys must be provided for ${req.method} on entity collections`, { status: 405 })
66
+ from.ref[0] = { id: from.ref[0], where: cds.ql.predicate(keys) }
57
67
  }
58
68
 
59
69
  const _isStream = isStream(req._query)
60
70
 
61
71
  if (_propertyAccess && req.method === 'PATCH' && !_isStream) {
62
- throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
72
+ throw new cds.error(`Method ${req.method} is not allowed for properties`, { status: 405 })
63
73
  }
64
74
 
65
75
  const model = cds.context.model ?? service.model