@sap/cds 7.8.2 → 7.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 +37 -0
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +3 -0
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_es_MX.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/bin/serve.js +5 -5
- package/lib/auth/basic-auth.js +1 -1
- package/lib/compile/cdsc.js +33 -6
- package/lib/compile/etc/_localized.js +14 -7
- package/lib/compile/for/lean_drafts.js +9 -0
- package/lib/compile/to/edm-files.js +116 -0
- package/lib/compile/to/edm.js +8 -1
- package/lib/compile/to/hdbtabledata.js +3 -3
- package/lib/compile/to/sql.js +4 -2
- package/lib/compile/to/yaml.js +22 -21
- package/lib/dbs/cds-deploy.js +5 -6
- package/lib/env/cds-env.js +7 -0
- package/lib/env/cds-requires.js +20 -1
- package/lib/env/defaults.js +21 -5
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +85 -4
- package/lib/index.js +1 -1
- package/lib/linked/entities.js +10 -0
- package/lib/linked/models.js +1 -1
- package/lib/plugins.js +1 -1
- package/lib/ql/INSERT.js +17 -3
- package/lib/ql/Query.js +4 -0
- package/lib/ql/infer.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/srv/cds-serve.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/protocols/odata-v4.js +5 -6
- package/lib/srv/srv-models.js +9 -2
- package/lib/utils/cds-test.js +2 -0
- package/lib/utils/cds-utils.js +9 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
- package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
- package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
- package/libx/_runtime/common/generic/auth/index.js +2 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
- package/libx/_runtime/common/generic/auth/restrict.js +6 -5
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +5 -8
- package/libx/_runtime/common/generic/etag.js +8 -6
- package/libx/_runtime/common/generic/sorting.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
- package/libx/_runtime/common/utils/compareJson.js +274 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
- package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
- package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
- package/libx/_runtime/common/utils/resolveView.js +0 -16
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +9 -2
- package/libx/_runtime/common/utils/ucsn.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/generic/rewrite.js +7 -13
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +151 -46
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/execute.js +6 -2
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
- package/libx/_runtime/messaging/event-broker.js +212 -0
- package/libx/_runtime/remote/Service.js +9 -32
- package/libx/_runtime/remote/utils/client.js +13 -21
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
- package/libx/_runtime/sqlite/execute.js +8 -3
- package/libx/_runtime/ucl/Service.js +259 -0
- package/libx/common/assert/index.js +5 -11
- package/libx/common/assert/validation.js +6 -1
- package/libx/odata/index.js +47 -25
- package/libx/odata/middleware/batch.js +8 -7
- package/libx/odata/middleware/create.js +42 -16
- package/libx/odata/middleware/delete.js +18 -11
- package/libx/odata/middleware/metadata.js +15 -14
- package/libx/odata/middleware/operation.js +30 -40
- package/libx/odata/middleware/parse.js +2 -3
- package/libx/odata/middleware/read.js +59 -52
- package/libx/odata/middleware/service-document.js +7 -7
- package/libx/odata/middleware/stream.js +26 -24
- package/libx/odata/middleware/update.js +53 -92
- package/libx/odata/parse/afterburner.js +45 -47
- package/libx/odata/parse/grammar.peggy +3 -3
- package/libx/odata/parse/multipartToJson.js +10 -22
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +13 -0
- package/libx/odata/utils/handler.js +120 -0
- package/libx/odata/utils/index.js +15 -2
- package/libx/odata/utils/metaInfo.js +410 -0
- package/libx/odata/utils/path.js +5 -2
- package/libx/odata/utils/readAfterWrite.js +23 -0
- package/libx/odata/utils/result.js +4 -5
- package/libx/rest/RestAdapter.js +4 -13
- package/libx/rest/middleware/parse.js +40 -7
- package/package.json +1 -1
- package/server.js +1 -0
- package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
- package/libx/_runtime/common/utils/thenable.js +0 -51
- package/libx/_runtime/rest/service.js +0 -2
- package/libx/odata/parse/parseToCqn.js +0 -39
- package/libx/rest/middleware/input.js +0 -54
- package/libx/rest/middleware/payload.js +0 -13
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
2
|
+
const LOG = cds.log('ucl')
|
|
3
|
+
|
|
4
|
+
const https = require('https')
|
|
5
|
+
|
|
6
|
+
class UCLService extends cds.Service {
|
|
7
|
+
async init() {
|
|
8
|
+
await super.init()
|
|
9
|
+
this.validate()
|
|
10
|
+
this._register()
|
|
11
|
+
this.agent = this.getAgent()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getAgent() {
|
|
15
|
+
try {
|
|
16
|
+
if (this.options.x509.certPath && this.options.x509.pkeyPath) {
|
|
17
|
+
return new https.Agent({
|
|
18
|
+
cert: cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.certPath)),
|
|
19
|
+
key: cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.pkeyPath))
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (LOG) LOG.error('GetCredentials', { error: error.message })
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _registerProvisioningEvents() {
|
|
29
|
+
var provisioning
|
|
30
|
+
try {
|
|
31
|
+
provisioning = await cds.connect.to('cds.xt.SaasProvisioningService')
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"Provisioning service 'cds.xt.SaasProvisioningService' can not be found, therefore mode is not multitenant. Single tenant applications are not supported."
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
if (provisioning) {
|
|
38
|
+
provisioning.prepend(() => {
|
|
39
|
+
provisioning.on('dependencies', async (_, next) => {
|
|
40
|
+
let dependencies = await next()
|
|
41
|
+
const xsappnameCMPClone = await this._getUCLDependency()
|
|
42
|
+
dependencies.push({ xsappname: xsappnameCMPClone })
|
|
43
|
+
return dependencies
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
validate() {
|
|
50
|
+
if (!this.options.namespace) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'UCL integrator requires an application namespace. You can set environment variable SAP_APPLICATION_NAMESPACE or you can give namespace as an option in your cds.requires section as described in documentation'
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
if (!cds.requires.multitenancy && cds.env.profile !== 'mtx-sidecar') {
|
|
56
|
+
throw new Error('[ucl] - Currently only multitenant applications are supported.')
|
|
57
|
+
}
|
|
58
|
+
if (!this.options.systemType || !this.options.systemDescription) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'systemType and systemDescription is obligatory parameters, please fill as shown in documentation'
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async readTemplate() {
|
|
66
|
+
const xsappname = this.options.credentials.xsappname
|
|
67
|
+
const query = `
|
|
68
|
+
query ($key: String!, $value: String!) {
|
|
69
|
+
applicationTemplates(filter: { key: $key, query: $value }) {
|
|
70
|
+
data {
|
|
71
|
+
id
|
|
72
|
+
name
|
|
73
|
+
description
|
|
74
|
+
placeholders {
|
|
75
|
+
name
|
|
76
|
+
description
|
|
77
|
+
}
|
|
78
|
+
applicationInput
|
|
79
|
+
labels
|
|
80
|
+
webhooks {
|
|
81
|
+
type
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
`
|
|
87
|
+
const variables = { key: 'xsappname', value: `"${xsappname}"` }
|
|
88
|
+
return (await this.request(query, variables)).applicationTemplates.data[0]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async _createTemplate() {
|
|
92
|
+
const xsappname = this.options.credentials.xsappname
|
|
93
|
+
const query = `mutation {
|
|
94
|
+
result: createApplicationTemplate (
|
|
95
|
+
in: {
|
|
96
|
+
name: "${this.options.systemType}"
|
|
97
|
+
description: "${this.options.systemDescription}"
|
|
98
|
+
applicationInput: {
|
|
99
|
+
name: "${this.options.systemType}"
|
|
100
|
+
description: "${this.options.systemDescription}"
|
|
101
|
+
providerName: "${this.options.provider}"
|
|
102
|
+
localTenantID: "{{tenant-id}}"
|
|
103
|
+
labels: {
|
|
104
|
+
displayName: "{{subdomain}}"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
placeholders: [
|
|
108
|
+
{ name: "subdomain", description: "The subdomain of the consumer tenant" }
|
|
109
|
+
{ name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.subscribedSubaccountId" }
|
|
110
|
+
]
|
|
111
|
+
labels: {
|
|
112
|
+
managed_app_provisioning: true
|
|
113
|
+
xsappname: "${xsappname}"
|
|
114
|
+
}
|
|
115
|
+
applicationNamespace: "${this.options.namespace}"
|
|
116
|
+
accessLevel: GLOBAL
|
|
117
|
+
}
|
|
118
|
+
) {
|
|
119
|
+
id
|
|
120
|
+
name
|
|
121
|
+
labels
|
|
122
|
+
applicationInput
|
|
123
|
+
applicationNamespace
|
|
124
|
+
}
|
|
125
|
+
}`
|
|
126
|
+
try {
|
|
127
|
+
return this.handleResponse(await this.request(query))
|
|
128
|
+
} catch (e) {
|
|
129
|
+
this.handleResponse(e)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
handleResponse(result) {
|
|
134
|
+
if (result.response && result.response.errors) {
|
|
135
|
+
let errorMessage = result.response.errors[0].message
|
|
136
|
+
throw new Error(errorMessage)
|
|
137
|
+
} else {
|
|
138
|
+
return result.result
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async deleteTemplate() {
|
|
143
|
+
const template = await this.readTemplate()
|
|
144
|
+
if (!template) return
|
|
145
|
+
const query = `mutation {
|
|
146
|
+
result: deleteApplicationTemplate(
|
|
147
|
+
id: "${template.id}"
|
|
148
|
+
){
|
|
149
|
+
id
|
|
150
|
+
name
|
|
151
|
+
description
|
|
152
|
+
}
|
|
153
|
+
}`
|
|
154
|
+
return this.handleResponse(await this.request(query))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async _getUCLDependency() {
|
|
158
|
+
if (!this.template) {
|
|
159
|
+
throw Error('Application template not found on UCL!')
|
|
160
|
+
}
|
|
161
|
+
return this.template.labels.xsappnameCMPClone
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Replace with fetch
|
|
165
|
+
async request(query, variables) {
|
|
166
|
+
const opts = {
|
|
167
|
+
host: this.options.host,
|
|
168
|
+
path: this.options.path,
|
|
169
|
+
agent: this.agent,
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
'Content-Type': 'application/json'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
const req = https.request(opts, res => {
|
|
177
|
+
const chunks = []
|
|
178
|
+
|
|
179
|
+
res.on('data', chunk => {
|
|
180
|
+
chunks.push(chunk)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
res.on('end', () => {
|
|
184
|
+
const response = {
|
|
185
|
+
statusCode: res.statusCode,
|
|
186
|
+
headers: res.headers,
|
|
187
|
+
body: Buffer.concat(chunks).toString()
|
|
188
|
+
}
|
|
189
|
+
resolve(JSON.parse(response.body).data)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
req.on('error', error => {
|
|
194
|
+
reject(error)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
if (query) {
|
|
198
|
+
req.write(JSON.stringify({ query, variables }))
|
|
199
|
+
}
|
|
200
|
+
req.end()
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async _registerApplicationTemplate() {
|
|
205
|
+
this.template = await this.readTemplate()
|
|
206
|
+
if (!this.template) {
|
|
207
|
+
LOG.info('Application Template cannot be found therefore created.')
|
|
208
|
+
await this._createTemplate()
|
|
209
|
+
} else {
|
|
210
|
+
await this._updateTemplate(this.template)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async _updateTemplate(template) {
|
|
215
|
+
const query = `mutation {
|
|
216
|
+
result: updateApplicationTemplate(
|
|
217
|
+
id: "${template.id}"
|
|
218
|
+
in: {
|
|
219
|
+
name: "${this.options.systemType}"
|
|
220
|
+
description: "${this.options.systemDescription}"
|
|
221
|
+
applicationInput: {
|
|
222
|
+
name: "${this.options.systemType}"
|
|
223
|
+
description: "${this.options.systemDescription}"
|
|
224
|
+
providerName: "${this.options.provider}"
|
|
225
|
+
localTenantID: "{{tenant-id}}"
|
|
226
|
+
labels: { displayName: "{{subdomain}}" }
|
|
227
|
+
}
|
|
228
|
+
applicationNamespace: "${this.options.namespace}"
|
|
229
|
+
placeholders: [
|
|
230
|
+
{ name: "subdomain", description: "The subdomain of the consumer tenant" }
|
|
231
|
+
{ name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.subscribedSubaccountId" }
|
|
232
|
+
]
|
|
233
|
+
accessLevel: GLOBAL
|
|
234
|
+
}
|
|
235
|
+
) {
|
|
236
|
+
id
|
|
237
|
+
name
|
|
238
|
+
description
|
|
239
|
+
applicationInput
|
|
240
|
+
}
|
|
241
|
+
}`
|
|
242
|
+
try {
|
|
243
|
+
const response = this.handleResponse(await this.request(query))
|
|
244
|
+
LOG.info('Application template updated successfully.')
|
|
245
|
+
return response
|
|
246
|
+
} catch (e) {
|
|
247
|
+
this.handleResponse(e)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_register() {
|
|
252
|
+
cds.once('listening', async () => {
|
|
253
|
+
await this._registerApplicationTemplate()
|
|
254
|
+
this._registerProvisioningEvents()
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = UCLService
|
|
@@ -2,14 +2,14 @@ const { cds } = global
|
|
|
2
2
|
|
|
3
3
|
const typeCheckers = require('./type')
|
|
4
4
|
const { checkMandatory, checkEnum, checkRange, checkFormat } = require('./validation')
|
|
5
|
-
const { getNested, getTarget, resolveCDSType, resolveSegment } = require('./utils')
|
|
5
|
+
const { getNested, getNormalizedDecimal, getTarget, resolveCDSType, resolveSegment } = require('./utils')
|
|
6
6
|
|
|
7
7
|
const NUMBER_TYPES = new Set(['cds.UInt8', 'cds.Int16', 'cds.Int32', 'cds.Integer', 'cds.Double'])
|
|
8
8
|
|
|
9
9
|
const _no_op = () => {}
|
|
10
10
|
|
|
11
11
|
const _reject_unknown = (_, k, def, errs) =>
|
|
12
|
-
errs.push(new cds.error(`Property ${k} does not exist in ${def.name}`, { statusCode: 400, code: '400' }))
|
|
12
|
+
errs.push(new cds.error(`Property "${k}" does not exist in ${def.name}`, { statusCode: 400, code: '400' }))
|
|
13
13
|
|
|
14
14
|
const _filter_unknown = (obj, k) => delete obj[k]
|
|
15
15
|
|
|
@@ -141,15 +141,9 @@ function _process(obj, def, errs, opts) {
|
|
|
141
141
|
// if used in protocol adapter, adjust val/ checker if necessary
|
|
142
142
|
if (opts.http) {
|
|
143
143
|
if (typeof v !== 'boolean') {
|
|
144
|
-
if (
|
|
145
|
-
else if (type === 'cds.
|
|
146
|
-
|
|
147
|
-
// REVISIT: consider ieee754 and exp dec headers?
|
|
148
|
-
// const ieee = opts.http.req?.headers['content-type'].match(/IEEE754Compatible=(\w+)/i)
|
|
149
|
-
// const exp = opts.http.req?.headers['content-type'].match(/ExponentialDecimals=(\w+)/i)
|
|
150
|
-
// if (type === 'cds.Decimal') {
|
|
151
|
-
// TODO
|
|
152
|
-
// }
|
|
144
|
+
if (type === 'cds.Decimal') v = getNormalizedDecimal(v)
|
|
145
|
+
else if (type === 'cds.Int64') v = String(v)
|
|
146
|
+
else if (NUMBER_TYPES.has(type)) v = Number(v)
|
|
153
147
|
}
|
|
154
148
|
}
|
|
155
149
|
|
|
@@ -73,7 +73,12 @@ const checkMandatory = (v, ele, errs, path, k) => {
|
|
|
73
73
|
const checkEnum = (v, ele, errs, path, k) => {
|
|
74
74
|
const enumElements = _getEnumElement(ele)
|
|
75
75
|
const enumValues = enumElements && _enumValues(enumElements)
|
|
76
|
-
|
|
76
|
+
const includes = (enumValues, v) => {
|
|
77
|
+
if (ele._type in { 'cds.Decimal': 1, 'cds.Int64': 1 }) {
|
|
78
|
+
return enumValues.map(ev => String(ev)).includes(String(v))
|
|
79
|
+
} else return enumValues.includes(v)
|
|
80
|
+
}
|
|
81
|
+
if (enumElements && !includes(enumValues, v)) {
|
|
77
82
|
const args =
|
|
78
83
|
typeof v === 'string'
|
|
79
84
|
? ['"' + v + '"', enumValues.map(ele => '"' + ele + '"').join(', ')]
|
package/libx/odata/index.js
CHANGED
|
@@ -9,28 +9,59 @@ const afterburner = require('./parse/afterburner')
|
|
|
9
9
|
const { getSafeNumber: safeNumber } = require('./utils')
|
|
10
10
|
const getError = require('../_runtime/common/error')
|
|
11
11
|
|
|
12
|
+
// used for function validation in peggy parser
|
|
13
|
+
// ----- should all be lowercase, as peggy compares to lowercase -----
|
|
14
|
+
// (plus: odata is case insensitive)
|
|
12
15
|
const strict = {
|
|
13
16
|
functions: {
|
|
17
|
+
// --- String + Collection: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360980
|
|
18
|
+
concat: 1,
|
|
14
19
|
contains: 1,
|
|
15
|
-
startswith: 1,
|
|
16
20
|
endswith: 1,
|
|
17
|
-
tolower: 1,
|
|
18
|
-
toupper: 1,
|
|
19
|
-
length: 1,
|
|
20
21
|
indexof: 1,
|
|
22
|
+
length: 1,
|
|
23
|
+
startswith: 1,
|
|
21
24
|
substring: 1,
|
|
25
|
+
// --- Collection: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360988
|
|
26
|
+
// REVISIT: not supported
|
|
27
|
+
// hassubset:1,
|
|
28
|
+
// hassubsequence:1,
|
|
29
|
+
// --- String: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360991
|
|
30
|
+
matchespattern: 1,
|
|
31
|
+
tolower: 1,
|
|
32
|
+
toupper: 1,
|
|
22
33
|
trim: 1,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
month: 1,
|
|
34
|
+
// --- Date + Time: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360996
|
|
35
|
+
date: 1,
|
|
26
36
|
day: 1,
|
|
37
|
+
fractionalseconds: 1,
|
|
27
38
|
hour: 1,
|
|
39
|
+
maxdatetime: 1,
|
|
40
|
+
mindatetime: 1,
|
|
28
41
|
minute: 1,
|
|
42
|
+
month: 1,
|
|
43
|
+
now: 1,
|
|
29
44
|
second: 1,
|
|
30
45
|
time: 1,
|
|
31
|
-
|
|
46
|
+
totaloffsetminutes: 1,
|
|
47
|
+
totalseconds: 1,
|
|
48
|
+
year: 1,
|
|
49
|
+
// --- Arithemetic: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31361011
|
|
50
|
+
ceiling: 1,
|
|
51
|
+
floor: 1,
|
|
32
52
|
round: 1,
|
|
33
|
-
|
|
53
|
+
// --- Type: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31361015
|
|
54
|
+
// REVISIT: not supported
|
|
55
|
+
// cast: 1,
|
|
56
|
+
// REVISIT: has to be implemented inside the odata adapter
|
|
57
|
+
// isof: 1,
|
|
58
|
+
// --- Geo: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31361018
|
|
59
|
+
// REVISIT: not supported
|
|
60
|
+
// 'geo.distance': 1,
|
|
61
|
+
// 'geo.intersects': 1,
|
|
62
|
+
// 'geo.length': 1,
|
|
63
|
+
// --- Conditional: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31361022
|
|
64
|
+
case: 1
|
|
34
65
|
}
|
|
35
66
|
}
|
|
36
67
|
|
|
@@ -67,10 +98,8 @@ module.exports = {
|
|
|
67
98
|
parse: (url, options = {}) => {
|
|
68
99
|
// first arg may also be req
|
|
69
100
|
if (url.url) url = url.url
|
|
70
|
-
// REVISIT: for okra, remove when no longer needed
|
|
71
|
-
else if (url.getIncomingRequest) url = url.getIncomingRequest().url
|
|
72
101
|
|
|
73
|
-
url = decodeURIComponent(url)
|
|
102
|
+
url = decodeURIComponent(url)
|
|
74
103
|
|
|
75
104
|
options = options === 'strict' ? { strict } : options.strict ? { ...options, strict } : options
|
|
76
105
|
if (options.service) Object.assign(options, { minimal: true, afterburner: afterburner.for(options.service) })
|
|
@@ -81,18 +110,13 @@ module.exports = {
|
|
|
81
110
|
try {
|
|
82
111
|
cqn = odata2cqn(url, options)
|
|
83
112
|
} catch (err) {
|
|
84
|
-
if (err.statusCode === 501)
|
|
85
|
-
throw getError(err.statusCode, err.message)
|
|
86
|
-
}
|
|
113
|
+
if (err.statusCode === 501) throw getError(err.statusCode, err.message)
|
|
87
114
|
|
|
88
115
|
let offset = err.location && err.location.start.offset
|
|
89
|
-
if (!offset && err.statusCode && err.message)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// we need to add the number of chars from base url to the offset
|
|
94
|
-
offset += options.baseUrl.length
|
|
95
|
-
}
|
|
116
|
+
if (!offset && err.statusCode && err.message) throw err
|
|
117
|
+
|
|
118
|
+
// we need to add the number of chars from base url to the offset
|
|
119
|
+
offset += options.baseUrl ? options.baseUrl.length : 0
|
|
96
120
|
|
|
97
121
|
// TODO adjust this to behave like above
|
|
98
122
|
err.message = `Parsing URL failed at position ${offset}: ${err.message}`
|
|
@@ -102,9 +126,7 @@ module.exports = {
|
|
|
102
126
|
|
|
103
127
|
// cqn is an array, if concat is used
|
|
104
128
|
if (Array.isArray(cqn)) {
|
|
105
|
-
for (let i = 0; i < cqn.length; i++)
|
|
106
|
-
cqn[i] = enhanceCqn(cqn[i], options)
|
|
107
|
-
}
|
|
129
|
+
for (let i = 0; i < cqn.length; i++) cqn[i] = enhanceCqn(cqn[i], options)
|
|
108
130
|
} else {
|
|
109
131
|
cqn = enhanceCqn(cqn, options)
|
|
110
132
|
}
|
|
@@ -4,6 +4,8 @@ const { AsyncResource } = require('async_hooks')
|
|
|
4
4
|
// eslint-disable-next-line cds/no-missing-dependencies
|
|
5
5
|
const express = require('express')
|
|
6
6
|
const { STATUS_CODES } = require('http')
|
|
7
|
+
const qs = require('querystring')
|
|
8
|
+
const { URL } = require('url')
|
|
7
9
|
|
|
8
10
|
const multipartToJson = require('../parse/multipartToJson')
|
|
9
11
|
|
|
@@ -35,6 +37,9 @@ const _validateBatch = body => {
|
|
|
35
37
|
|
|
36
38
|
_validateProperty('requests', requests, 'Array')
|
|
37
39
|
|
|
40
|
+
if (requests.length > cds.env.odata.batch_limit)
|
|
41
|
+
cds.error('BATCH_TOO_MANY_REQ', { code: 'BATCH_TOO_MANY_REQ', statusCode: 429 })
|
|
42
|
+
|
|
38
43
|
const ids = {}
|
|
39
44
|
|
|
40
45
|
let previousAtomicityGroup
|
|
@@ -115,15 +120,11 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
115
120
|
|
|
116
121
|
req.method = method.toUpperCase()
|
|
117
122
|
req.url = url
|
|
118
|
-
|
|
123
|
+
const u = new URL(url, 'http://cap')
|
|
124
|
+
req.query = qs.parse(u.search.slice(1))
|
|
119
125
|
req.headers = request.headers || {}
|
|
120
126
|
req.body = request.body
|
|
121
127
|
|
|
122
|
-
// propagate user, tenant and locale
|
|
123
|
-
req.user = _req.user
|
|
124
|
-
req.tenant = _req.tenant
|
|
125
|
-
req.locale = _req.locale
|
|
126
|
-
|
|
127
128
|
const res = (ret.res = new express.response.constructor(req))
|
|
128
129
|
res.__proto__ = express.response
|
|
129
130
|
|
|
@@ -270,7 +271,7 @@ const _formatResponseMultipart = (request, res, boundary) => {
|
|
|
270
271
|
let meta = [],
|
|
271
272
|
data = []
|
|
272
273
|
for (const [k, v] of Object.entries(_json)) {
|
|
273
|
-
if (k.startsWith('@')) meta.push(`"${k}":"${v}"`)
|
|
274
|
+
if (k.startsWith('@')) meta.push(`"${k}":"${v.replaceAll('"', '\\"')}"`)
|
|
274
275
|
else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
|
|
275
276
|
}
|
|
276
277
|
const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
|
|
@@ -2,13 +2,18 @@ const cds = require('../../../')
|
|
|
2
2
|
const { INSERT } = cds.ql
|
|
3
3
|
|
|
4
4
|
const { toODataResult, postProcess } = require('../utils/result')
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
calculateLocationHeader,
|
|
7
|
+
getKeysAndParamsFromPath,
|
|
8
|
+
handleSapMessages,
|
|
9
|
+
getPreferReturnHeader
|
|
10
|
+
} = require('../utils')
|
|
11
|
+
const { getDeepSelect, getSimpleSelectCQN } = require('../utils/handler')
|
|
6
12
|
|
|
7
13
|
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
15
|
+
const readAfterWrite = require('../utils/readAfterWrite')
|
|
16
|
+
const metaInfo = require('../utils/metaInfo')
|
|
12
17
|
|
|
13
18
|
module.exports = srv =>
|
|
14
19
|
function create(req, res, next) {
|
|
@@ -17,12 +22,12 @@ module.exports = srv =>
|
|
|
17
22
|
target
|
|
18
23
|
} = req._query
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
25
|
+
// req.__proto__.method is set in case of upsert
|
|
26
|
+
const isUpsert = req.__proto__.method in { PUT: 1, PATCH: 1 }
|
|
27
|
+
|
|
28
|
+
if (one && !isUpsert) {
|
|
29
|
+
const msg = 'Method POST is not allowed for singletons and individual entities'
|
|
30
|
+
throw Object.assign(new Error(msg), { statusCode: 405 })
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
// payload & params
|
|
@@ -42,10 +47,17 @@ module.exports = srv =>
|
|
|
42
47
|
// query
|
|
43
48
|
const query = INSERT.into(from).entries(data)
|
|
44
49
|
|
|
50
|
+
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
51
|
+
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
52
|
+
|
|
45
53
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
46
|
-
const cdsReq = new cds.Request({ query, params, req, res })
|
|
54
|
+
const cdsReq = new cds.Request({ query, params, headers, req, res })
|
|
47
55
|
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
48
56
|
|
|
57
|
+
// API for subrequests of $batch (or incoming request)
|
|
58
|
+
cdsReq.req = req
|
|
59
|
+
cdsReq.res = res
|
|
60
|
+
|
|
49
61
|
// rewrite event for draft-enabled entities
|
|
50
62
|
if (target._isDraftEnabled) cdsReq.event = 'NEW'
|
|
51
63
|
|
|
@@ -57,8 +69,12 @@ module.exports = srv =>
|
|
|
57
69
|
return srv.dispatch(cdsReq).then(result => {
|
|
58
70
|
handleSapMessages(cdsReq, req, res)
|
|
59
71
|
|
|
72
|
+
// generic handlers indicate that read after write is required
|
|
60
73
|
if (cdsReq._.readAfterWrite) {
|
|
61
|
-
|
|
74
|
+
// const keys = cdsReq.target.keys?.filter(k => !k.isAssociation)?.reduce((prev, k) => { prev[k] = result[k]; return prev}, {} )
|
|
75
|
+
// const query = SELECT.one(cdsReq.query.INSERT.into, keys)
|
|
76
|
+
const query = cdsReq.event === 'NEW' ? getSimpleSelectCQN(cdsReq.target, result) : getDeepSelect(cdsReq)
|
|
77
|
+
return readAfterWrite(cdsReq, srv, query)
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
return result
|
|
@@ -66,15 +82,25 @@ module.exports = srv =>
|
|
|
66
82
|
})
|
|
67
83
|
.then(result => {
|
|
68
84
|
// we use an extra then block, after getting the result, so the transaction is commited, before sending the response
|
|
85
|
+
|
|
86
|
+
// determine calculation based on result with req.data as fallback
|
|
87
|
+
if (!target._isSingleton)
|
|
88
|
+
res.set('location', calculateLocationHeader(cdsReq.target, srv, result || cdsReq.data))
|
|
89
|
+
|
|
69
90
|
if (result == null) return res.sendStatus(204)
|
|
70
|
-
const isMinimal = req
|
|
91
|
+
const isMinimal = getPreferReturnHeader(req) === 'minimal'
|
|
71
92
|
postProcess(cdsReq.target, srv, result, isMinimal)
|
|
93
|
+
|
|
72
94
|
if (result['$etag']) res.set('etag', result['$etag'])
|
|
73
|
-
res.set('location', calculateLocationHeader(cdsReq.target, srv, result))
|
|
74
95
|
if (isMinimal) return res.sendStatus(204)
|
|
96
|
+
|
|
75
97
|
const info = metaInfo(query, 'CREATE', srv, result, req)
|
|
76
98
|
result = toODataResult(result, info)
|
|
77
|
-
res.
|
|
99
|
+
res.set('content-type', 'application/json;IEEE754Compatible=true')
|
|
100
|
+
res.status(201).send(result)
|
|
101
|
+
})
|
|
102
|
+
.catch(err => {
|
|
103
|
+
handleSapMessages(cdsReq, req, res)
|
|
104
|
+
next(err)
|
|
78
105
|
})
|
|
79
|
-
.catch(next) // should be outside, so tx can be rolled back in case of errors
|
|
80
106
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
const { UPDATE, DELETE } = cds.ql
|
|
3
3
|
|
|
4
|
-
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
4
|
+
const { getKeysAndParamsFromPath, handleSapMessages, getPreferReturnHeader } = require('../utils')
|
|
5
5
|
|
|
6
6
|
module.exports = srv =>
|
|
7
7
|
function deleete(req, res, next) {
|
|
8
|
-
if (req
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
})
|
|
8
|
+
if (getPreferReturnHeader(req)) {
|
|
9
|
+
const msg = "The 'return' preference is not allowed in DELETE requests"
|
|
10
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
// REVISIT: better solution for query._propertyAccess
|
|
@@ -19,8 +18,7 @@ module.exports = srv =>
|
|
|
19
18
|
} = req._query
|
|
20
19
|
|
|
21
20
|
if (!one) {
|
|
22
|
-
|
|
23
|
-
throw Object.assign(new Error('Method DELETE not allowed for ENTITY.COLLECTION'), { statusCode: 405 })
|
|
21
|
+
throw Object.assign(new Error('Method DELETE is not allowed for entity collections'), { statusCode: 405 })
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
// payload & params
|
|
@@ -31,20 +29,29 @@ module.exports = srv =>
|
|
|
31
29
|
// query
|
|
32
30
|
const query = _propertyAccess ? UPDATE(from).set({ [_propertyAccess]: null }) : DELETE.from(from)
|
|
33
31
|
|
|
32
|
+
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
33
|
+
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
34
|
+
|
|
34
35
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
35
|
-
const cdsReq = new cds.Request({ query, data, params, req, res })
|
|
36
|
+
const cdsReq = new cds.Request({ query, data, headers, params, req, res })
|
|
36
37
|
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
37
38
|
|
|
39
|
+
// API for subrequests of $batch (or incoming request)
|
|
40
|
+
cdsReq.req = req
|
|
41
|
+
cdsReq.res = res
|
|
42
|
+
|
|
38
43
|
// rewrite event for draft-enabled entities
|
|
39
44
|
if (target._isDraftEnabled && cdsReq.data.IsActiveEntity === false) cdsReq.event = 'CANCEL'
|
|
40
45
|
|
|
41
46
|
return srv
|
|
42
47
|
.dispatch(cdsReq)
|
|
43
|
-
.then(
|
|
48
|
+
.then(() => {
|
|
44
49
|
handleSapMessages(cdsReq, req, res)
|
|
45
50
|
|
|
46
|
-
if (result === 0) throw Object.assign(new Error('Not found'), { statusCode: 404 })
|
|
47
51
|
res.sendStatus(204)
|
|
48
52
|
})
|
|
49
|
-
.catch(
|
|
53
|
+
.catch(err => {
|
|
54
|
+
handleSapMessages(cdsReq, req, res)
|
|
55
|
+
next(err)
|
|
56
|
+
})
|
|
50
57
|
}
|