@sap/cds 8.8.2 → 8.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -4
- package/_i18n/i18n_en_US_saptrc.properties +3 -0
- package/bin/colors.js +2 -0
- package/bin/test.js +103 -75
- package/eslint.config.mjs +16 -4
- package/lib/compile/for/lean_drafts.js +4 -0
- package/lib/compile/parse.js +26 -6
- package/lib/env/cds-env.js +3 -1
- package/lib/env/cds-requires.js +0 -3
- package/lib/env/schemas/cds-rc.js +11 -0
- package/lib/log/format/aspects/cls.js +2 -1
- package/lib/log/format/json.js +1 -1
- package/lib/plugins.js +2 -3
- package/lib/ql/SELECT.js +2 -1
- package/lib/ql/cds-ql.js +2 -0
- package/lib/ql/cds.ql-predicates.js +6 -4
- package/lib/ql/resolve.js +46 -0
- package/lib/req/validate.js +1 -0
- package/lib/srv/bindings.js +64 -43
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/cds-serve.js +2 -2
- package/lib/srv/middlewares/auth/ias-auth.js +2 -0
- package/lib/srv/protocols/http.js +2 -2
- package/lib/srv/protocols/index.js +1 -1
- package/lib/srv/protocols/odata-v4.js +0 -1
- package/lib/srv/srv-tx.js +1 -1
- package/lib/test/cds-test.js +3 -4
- package/lib/utils/cds-utils.js +19 -19
- package/lib/utils/colors.js +46 -45
- package/lib/utils/csv-reader.js +5 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
- package/libx/_runtime/common/Service.js +4 -2
- package/libx/_runtime/common/composition/data.js +1 -2
- package/libx/_runtime/common/composition/tree.js +6 -4
- package/libx/_runtime/common/generic/sorting.js +6 -2
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +6 -7
- package/libx/_runtime/common/utils/differ.js +1 -1
- package/libx/_runtime/common/utils/draft.js +1 -1
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +6 -2
- package/libx/_runtime/common/utils/keys.js +13 -84
- package/libx/_runtime/common/utils/propagateForeignKeys.js +4 -3
- package/libx/_runtime/common/utils/resolveView.js +96 -102
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/stream.js +2 -3
- package/libx/_runtime/db/utils/columns.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +11 -7
- package/libx/_runtime/messaging/common-utils/connections.js +6 -2
- package/libx/_runtime/messaging/kafka.js +3 -4
- package/libx/_runtime/remote/Service.js +13 -5
- package/libx/_runtime/remote/utils/client.js +1 -0
- package/libx/_runtime/ucl/Service.js +135 -126
- package/libx/common/utils/path.js +34 -22
- package/libx/odata/middleware/create.js +2 -0
- package/libx/odata/middleware/operation.js +8 -2
- package/libx/odata/middleware/parse.js +1 -1
- package/libx/odata/middleware/stream.js +1 -2
- package/libx/odata/middleware/update.js +2 -0
- package/libx/odata/parse/afterburner.js +17 -9
- package/libx/odata/parse/cqn2odata.js +3 -1
- package/libx/odata/parse/grammar.peggy +21 -19
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/metadata.js +8 -2
- package/libx/odata/utils/odataBind.js +36 -0
- package/libx/outbox/index.js +1 -0
- package/libx/rest/middleware/operation.js +9 -8
- package/libx/rest/middleware/parse.js +1 -0
- package/package.json +3 -3
- package/lib/i18n/resources.js +0 -150
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
if (
|
|
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
|
|
@@ -393,6 +393,8 @@ function $orderBy(orderBy) {
|
|
|
393
393
|
const res = []
|
|
394
394
|
|
|
395
395
|
for (const cur of orderBy) {
|
|
396
|
+
if (cur.implicit) continue
|
|
397
|
+
|
|
396
398
|
if (hasValidProps(cur, 'ref', 'sort')) {
|
|
397
399
|
res.push(_format(cur) + '%20' + cur.sort)
|
|
398
400
|
continue
|
|
@@ -412,7 +414,7 @@ function $orderBy(orderBy) {
|
|
|
412
414
|
}
|
|
413
415
|
}
|
|
414
416
|
|
|
415
|
-
return '$orderby=' + res.join(',')
|
|
417
|
+
if (res.length) return '$orderby=' + res.join(',')
|
|
416
418
|
}
|
|
417
419
|
|
|
418
420
|
function parseSearch(search) {
|
|
@@ -210,13 +210,18 @@
|
|
|
210
210
|
|
|
211
211
|
}
|
|
212
212
|
if (apply.topLevels.levels) {
|
|
213
|
-
cqn.recurse.where = [{ ref: ['DistanceFromRoot'] }, '<=', { val: apply.topLevels.levels }]
|
|
213
|
+
cqn.recurse.where = [{ ref: ['DistanceFromRoot'] }, '<=', { val: apply.topLevels.levels - 1 }]
|
|
214
214
|
}
|
|
215
215
|
if (apply.topLevels.expandLevels) {
|
|
216
216
|
if (!cqn.recurse.where) cqn.recurse.where = []
|
|
217
217
|
for (const expandLevel of apply.topLevels.expandLevels) {
|
|
218
218
|
if (cqn.recurse.where.length !== 0) cqn.recurse.where.push('or')
|
|
219
|
-
cqn.recurse.where.push({ ref: [apply.topLevels.nodeProperty] }, '=', { val: expandLevel.nodeID }
|
|
219
|
+
cqn.recurse.where.push({ ref: [apply.topLevels.nodeProperty] }, '=', { val: expandLevel.nodeID })
|
|
220
|
+
if (Number.isInteger(expandLevel.levels)) {
|
|
221
|
+
cqn.recurse.where.push('and', { ref: ['Distance'] })
|
|
222
|
+
if (expandLevel.levels === 1) cqn.recurse.where.push('=', { val: 1 })
|
|
223
|
+
else cqn.recurse.where.push('between', { val: 1 }, 'and', { val: expandLevel.levels } )
|
|
224
|
+
}
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
}
|
|
@@ -244,23 +249,16 @@
|
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
if (apply.ancestors && apply.topLevels) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
_ancestors(inner.SELECT)
|
|
253
|
-
cqn.from = inner
|
|
254
|
-
_toplevels(cqn)
|
|
252
|
+
_ancestors(cqn)
|
|
253
|
+
const extra = {...cqn}
|
|
254
|
+
_toplevels(extra)
|
|
255
|
+
cqn.recurse = extra.recurse
|
|
255
256
|
} else if (apply.ancestors && apply.descendants) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
_ancestors(inner.SELECT)
|
|
262
|
-
cqn.from = inner
|
|
263
|
-
_descendants(cqn)
|
|
257
|
+
_ancestors(cqn)
|
|
258
|
+
const extra = {...cqn}
|
|
259
|
+
_descendants(extra)
|
|
260
|
+
cqn.where = extra.where
|
|
261
|
+
cqn.recurse = extra.recurse
|
|
264
262
|
} else if (apply.topLevels) {
|
|
265
263
|
_toplevels(cqn)
|
|
266
264
|
} else if (apply.ancestors) {
|
|
@@ -1019,7 +1017,11 @@
|
|
|
1019
1017
|
|
|
1020
1018
|
expandItemProp
|
|
1021
1019
|
= "\"NodeID\"" o ":" o s:doubleQuotedString{return { nodeID: s } } /
|
|
1022
|
-
"\"Levels\"" o ":" o
|
|
1020
|
+
"\"Levels\"" o ":" o l:expandItemLevel{return { levels: l } }
|
|
1021
|
+
|
|
1022
|
+
expandItemLevel
|
|
1023
|
+
= i:integer{ return i } /
|
|
1024
|
+
null{ return null }
|
|
1023
1025
|
|
|
1024
1026
|
ansDescTrafo
|
|
1025
1027
|
= OPEN o "$root/" p:simplePath o COMMA o h:identifier o COMMA o e:identifier o COMMA? t:ansDescGetNodes? o COMMA? d:integer? o COMMA? k:ansDescKeepStart? CLOSE {
|