@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.
- package/CHANGELOG.md +85 -1
- package/_i18n/i18n_es.properties +3 -3
- package/_i18n/i18n_es_MX.properties +3 -3
- package/_i18n/i18n_fr.properties +2 -2
- package/_i18n/messages.properties +6 -0
- package/app/index.js +0 -1
- package/bin/deploy.js +1 -1
- package/bin/serve.js +7 -20
- package/lib/compile/cdsc.js +3 -0
- package/lib/compile/for/flows.js +102 -0
- package/lib/compile/for/nodejs.js +28 -0
- package/lib/compile/to/edm.js +11 -4
- package/lib/core/classes.js +1 -1
- package/lib/core/linked-csn.js +8 -0
- package/lib/dbs/cds-deploy.js +12 -12
- package/lib/env/cds-env.js +1 -1
- package/lib/env/cds-requires.js +21 -20
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +5 -6
- package/lib/log/cds-log.js +6 -5
- package/lib/log/format/aspects/cf.js +2 -2
- package/lib/plugins.js +1 -1
- package/lib/ql/cds-ql.js +0 -3
- package/lib/req/request.js +3 -3
- package/lib/req/response.js +12 -7
- package/lib/srv/bindings.js +17 -17
- package/lib/srv/cds-connect.js +6 -9
- package/lib/srv/cds-serve.js +74 -137
- package/lib/srv/cds.Service.js +49 -0
- package/lib/srv/factory.js +4 -4
- package/lib/srv/middlewares/auth/ias-auth.js +33 -9
- package/lib/srv/middlewares/auth/index.js +3 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +20 -6
- package/lib/srv/protocols/hcql.js +16 -1
- package/lib/srv/srv-dispatch.js +1 -1
- package/lib/utils/cds-utils.js +4 -8
- package/lib/utils/csv-reader.js +27 -7
- package/libx/_runtime/cds.js +0 -6
- package/libx/_runtime/common/Service.js +5 -0
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/flows.js +106 -0
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/utils/differ.js +5 -15
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/rewriteAsterisks.js +10 -4
- package/libx/_runtime/fiori/lean-draft.js +76 -40
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/messaging/service.js +7 -0
- package/libx/_runtime/remote/Service.js +68 -62
- package/libx/_runtime/remote/utils/client.js +29 -216
- package/libx/_runtime/remote/utils/query.js +197 -0
- package/libx/_runtime/ucl/Service.js +180 -112
- package/libx/_runtime/ucl/queries.js +61 -0
- package/libx/odata/ODataAdapter.js +1 -4
- package/libx/odata/index.js +2 -10
- package/libx/odata/middleware/error.js +8 -1
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/middleware/update.js +12 -2
- package/libx/odata/parse/afterburner.js +113 -20
- package/libx/odata/parse/cqn2odata.js +1 -3
- package/libx/rest/middleware/parse.js +9 -2
- package/package.json +2 -2
- package/server.js +2 -0
- package/srv/app-service.js +1 -0
- package/srv/db-service.js +1 -0
- package/srv/msg-service.js +1 -0
- package/srv/remote-service.js +1 -0
- package/srv/ucl-service.cds +32 -0
- package/srv/ucl-service.js +1 -0
- package/lib/ql/resolve.js +0 -45
- package/libx/common/assert/type-strict.js +0 -109
- 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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
32
|
-
|
|
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.
|
|
37
|
-
const template = existingTemplate ? await this.
|
|
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:
|
|
57
|
-
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
|
|
104
|
-
const 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
|
|
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
|
|
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
|
|
131
|
-
const template = await this.
|
|
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('>',
|
|
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
|
}
|
package/libx/odata/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
const cds = require('../..')
|
|
4
4
|
const { decodeURIComponent } = cds.utils
|
|
5
5
|
|
|
6
|
-
const odata2cqn = require('./parse/parser')
|
|
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 = {
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|