@sap/cds 7.7.3 → 7.8.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 +31 -1
- package/lib/auth/ias-auth.js +5 -3
- package/lib/auth/jwt-auth.js +4 -2
- package/lib/compile/cdsc.js +0 -10
- package/lib/compile/for/java.js +9 -5
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/to/edm.js +2 -1
- package/lib/compile/to/sql.js +0 -21
- package/lib/compile/to/srvinfo.js +13 -4
- package/lib/dbs/cds-deploy.js +7 -7
- package/lib/env/cds-requires.js +6 -0
- package/lib/index.js +4 -3
- package/lib/linked/classes.js +151 -88
- package/lib/linked/entities.js +28 -23
- package/lib/linked/models.js +57 -36
- package/lib/linked/types.js +42 -104
- package/lib/ql/Whereable.js +3 -3
- package/lib/req/context.js +9 -5
- package/lib/srv/protocols/hcql.js +2 -1
- package/lib/srv/protocols/http.js +7 -7
- package/lib/srv/protocols/index.js +31 -13
- package/lib/srv/protocols/odata-v4.js +79 -58
- package/lib/srv/srv-api.js +7 -6
- package/lib/srv/srv-dispatch.js +1 -12
- package/lib/srv/srv-tx.js +9 -13
- package/lib/utils/cds-utils.js +6 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +11 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +21 -12
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -7
- package/libx/_runtime/cds-services/services/utils/columns.js +6 -3
- package/libx/_runtime/cds.js +0 -13
- package/libx/_runtime/common/generic/input.js +3 -0
- package/libx/_runtime/common/generic/sorting.js +8 -6
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/cqn.js +5 -0
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +7 -1
- package/libx/_runtime/common/utils/keys.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +2 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/stream.js +0 -10
- package/libx/_runtime/common/utils/template.js +20 -35
- package/libx/_runtime/db/Service.js +5 -1
- package/libx/_runtime/db/utils/columns.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +14 -2
- package/libx/_runtime/messaging/Outbox.js +7 -5
- package/libx/_runtime/messaging/kafka.js +266 -0
- package/libx/_runtime/messaging/service.js +7 -5
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
- package/libx/common/assert/validation.js +1 -1
- package/libx/odata/index.js +8 -2
- package/libx/odata/middleware/batch.js +340 -0
- package/libx/odata/middleware/create.js +43 -46
- package/libx/odata/middleware/delete.js +27 -15
- package/libx/odata/middleware/error.js +6 -5
- package/libx/odata/middleware/metadata.js +16 -15
- package/libx/odata/middleware/operation.js +107 -59
- package/libx/odata/middleware/parse.js +15 -7
- package/libx/odata/middleware/read.js +150 -24
- package/libx/odata/middleware/service-document.js +17 -6
- package/libx/odata/middleware/stream.js +34 -17
- package/libx/odata/middleware/update.js +123 -87
- package/libx/odata/parse/afterburner.js +131 -28
- package/libx/odata/parse/cqn2odata.js +1 -1
- package/libx/odata/parse/grammar.peggy +4 -5
- package/libx/odata/parse/multipartToJson.js +163 -0
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +29 -47
- package/libx/odata/utils/path.js +72 -0
- package/libx/odata/utils/result.js +123 -20
- package/package.json +1 -1
- package/server.js +4 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
const cds = require('../../../')
|
|
2
|
+
const { AsyncResource } = require('async_hooks')
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line cds/no-missing-dependencies
|
|
5
|
+
const express = require('express')
|
|
6
|
+
const { STATUS_CODES } = require('http')
|
|
7
|
+
|
|
8
|
+
const multipartToJson = require('../parse/multipartToJson')
|
|
9
|
+
|
|
10
|
+
const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
|
|
11
|
+
const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
|
|
12
|
+
const CRLF = '\r\n'
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* common
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const _deserializationError = message => cds.error(`Deserialization Error: ${message}`, { code: 400 })
|
|
19
|
+
|
|
20
|
+
// Function must be called with an object containing exactly one key-value pair representing the property name and its value
|
|
21
|
+
const _validateProperty = (name, value, type) => {
|
|
22
|
+
if (value === undefined) throw _deserializationError(`Parameter '${name}' must not be undefined.`)
|
|
23
|
+
|
|
24
|
+
switch (type) {
|
|
25
|
+
case 'Array':
|
|
26
|
+
if (!Array.isArray(value)) throw _deserializationError(`Parameter '${name}' must be type of '${type}'.`)
|
|
27
|
+
break
|
|
28
|
+
default:
|
|
29
|
+
if (typeof value !== type) throw _deserializationError(`Parameter '${name}' must be type of '${type}'.`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const _validateBatch = body => {
|
|
34
|
+
const { requests } = body
|
|
35
|
+
|
|
36
|
+
_validateProperty('requests', requests, 'Array')
|
|
37
|
+
|
|
38
|
+
const ids = {}
|
|
39
|
+
|
|
40
|
+
let previousAtomicityGroup
|
|
41
|
+
requests.forEach((request, i) => {
|
|
42
|
+
if (typeof request !== 'object')
|
|
43
|
+
throw _deserializationError(`Element of 'requests' array at index ${i} must be type of 'object'.`)
|
|
44
|
+
|
|
45
|
+
const { id, method, url, body, atomicityGroup, dependsOn } = request
|
|
46
|
+
|
|
47
|
+
_validateProperty('id', id, 'string')
|
|
48
|
+
|
|
49
|
+
if (ids[id]) throw _deserializationError(`Request ID '${id}' is not unique.`)
|
|
50
|
+
else ids[id] = request
|
|
51
|
+
|
|
52
|
+
// TODO: validate allowed methods or let express throw the error?
|
|
53
|
+
_validateProperty('method', method, 'string')
|
|
54
|
+
if (!(method.toUpperCase() in HTTP_METHODS))
|
|
55
|
+
throw _deserializationError(`Method '${method}' is not allowed. Only DELETE, GET, PATCH, POST or PUT are.`)
|
|
56
|
+
|
|
57
|
+
_validateProperty('url', url, 'string')
|
|
58
|
+
// TODO: need similar validation in multipart/mixed batch
|
|
59
|
+
if (url.startsWith('/$batch')) throw _deserializationError('Nested batch requests are not allowed.')
|
|
60
|
+
|
|
61
|
+
// TODO: support for non JSON bodies?
|
|
62
|
+
if (body !== undefined && typeof body !== 'object')
|
|
63
|
+
throw _deserializationError('A Content-Type header has to be specified for a non JSON body.')
|
|
64
|
+
|
|
65
|
+
// TODO
|
|
66
|
+
// if (!(method.toUpperCase() in { GET: 1, DELETE: 1 }) && !body)
|
|
67
|
+
// throw _deserializationError(`Body is required for ${method} requests.`)
|
|
68
|
+
|
|
69
|
+
if (atomicityGroup) {
|
|
70
|
+
_validateProperty('atomicityGroup', atomicityGroup, 'string')
|
|
71
|
+
|
|
72
|
+
// All request objects with the same value for atomicityGroup MUST be adjacent in the requests array
|
|
73
|
+
if (atomicityGroup !== previousAtomicityGroup) {
|
|
74
|
+
if (ids[atomicityGroup]) throw _deserializationError(`Atomicity group ID '${atomicityGroup}' is not unique.`)
|
|
75
|
+
else ids[atomicityGroup] = [request]
|
|
76
|
+
} else {
|
|
77
|
+
ids[atomicityGroup].push(request)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (dependsOn) {
|
|
82
|
+
_validateProperty('dependsOn', dependsOn, 'Array')
|
|
83
|
+
dependsOn.forEach(dependsOnId => {
|
|
84
|
+
_validateProperty('dependent request ID', dependsOnId, 'string')
|
|
85
|
+
|
|
86
|
+
const dependency = ids[dependsOnId]
|
|
87
|
+
if (!dependency)
|
|
88
|
+
throw _deserializationError(`Request ID '${dependsOnId}' used in dependsOn has not been defined before.`)
|
|
89
|
+
|
|
90
|
+
const dependencyAtomicityGroup = dependency.atomicityGroup
|
|
91
|
+
if (dependencyAtomicityGroup && !dependsOn.includes(dependencyAtomicityGroup))
|
|
92
|
+
throw _deserializationError(
|
|
93
|
+
`The group '${dependencyAtomicityGroup}' of the referenced request '${dependsOnId}' must be listed in dependsOn of request '${id}'.`
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// TODO: validate if, and headers
|
|
99
|
+
|
|
100
|
+
previousAtomicityGroup = atomicityGroup
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return ids
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
107
|
+
const { id, method, url } = request
|
|
108
|
+
const ret = { id }
|
|
109
|
+
|
|
110
|
+
const req = (ret.req = new express.request.constructor())
|
|
111
|
+
req.__proto__ = express.request
|
|
112
|
+
|
|
113
|
+
// express internals
|
|
114
|
+
req.app = _req.app
|
|
115
|
+
|
|
116
|
+
req.method = method.toUpperCase()
|
|
117
|
+
req.url = url
|
|
118
|
+
req.query = {}
|
|
119
|
+
req.headers = request.headers || {}
|
|
120
|
+
req.body = request.body
|
|
121
|
+
|
|
122
|
+
// propagate user, tenant and locale
|
|
123
|
+
req.user = _req.user
|
|
124
|
+
req.tenant = _req.tenant
|
|
125
|
+
req.locale = _req.locale
|
|
126
|
+
|
|
127
|
+
const res = (ret.res = new express.response.constructor(req))
|
|
128
|
+
res.__proto__ = express.response
|
|
129
|
+
|
|
130
|
+
// express internals
|
|
131
|
+
res.app = _res.app
|
|
132
|
+
|
|
133
|
+
// back link
|
|
134
|
+
req.res = res
|
|
135
|
+
|
|
136
|
+
// resolve promise for subrequest via res.end()
|
|
137
|
+
ret.promise = new Promise((resolve, _reject) => {
|
|
138
|
+
res.end = (chunk, encoding) => {
|
|
139
|
+
res._chunk = chunk
|
|
140
|
+
res._encoding = encoding
|
|
141
|
+
resolve(ret)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return ret
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const _transaction = async (srv, router) => {
|
|
149
|
+
return new Promise(res => {
|
|
150
|
+
const ret = {}
|
|
151
|
+
srv.tx(
|
|
152
|
+
async () =>
|
|
153
|
+
(ret.promise = new Promise((resolve, reject) => {
|
|
154
|
+
const proms = []
|
|
155
|
+
ret.add = AsyncResource.bind(function (request, req, res) {
|
|
156
|
+
const lookalike = _createExpressReqResLookalike(request, req, res)
|
|
157
|
+
router.handle(lookalike.req, lookalike.res)
|
|
158
|
+
request.promise = lookalike.promise
|
|
159
|
+
proms.push(request.promise)
|
|
160
|
+
return request.promise
|
|
161
|
+
})
|
|
162
|
+
ret.done = function () {
|
|
163
|
+
return Promise.allSettled(proms).then(resolve, reject)
|
|
164
|
+
}
|
|
165
|
+
res(ret)
|
|
166
|
+
}))
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const _processBatch = async (srv, router, req, res, next, body, ct, boundary) => {
|
|
172
|
+
body ??= req.body
|
|
173
|
+
ct ??= 'JSON'
|
|
174
|
+
const isJson = ct === 'JSON'
|
|
175
|
+
const _formatResponse = isJson ? _formatResponseJson : _formatResponseMultipart
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const ids = _validateBatch(body) // REVISIT: we will not be able to validate the whole once we stream
|
|
179
|
+
|
|
180
|
+
// TODO: if (!requests || !requests.length) throw new Error('At least one request, buddy!')
|
|
181
|
+
|
|
182
|
+
let previousAtomicityGroup
|
|
183
|
+
let separator
|
|
184
|
+
let tx
|
|
185
|
+
|
|
186
|
+
res.setHeader('content-type', CT[ct] + (!isJson ? ';boundary=' + boundary : ''))
|
|
187
|
+
res.setHeader('OData-Version', '4.0') //> REVISIT: Fiori/ UI5 wants this
|
|
188
|
+
res.status(200)
|
|
189
|
+
res.write(isJson ? '{"responses":[' : '')
|
|
190
|
+
|
|
191
|
+
const { requests } = body
|
|
192
|
+
for await (const request of requests) {
|
|
193
|
+
const { atomicityGroup } = request
|
|
194
|
+
|
|
195
|
+
if (!atomicityGroup || atomicityGroup !== previousAtomicityGroup) {
|
|
196
|
+
if (tx) await tx.done()
|
|
197
|
+
tx = await _transaction(srv, router)
|
|
198
|
+
if (atomicityGroup) ids[atomicityGroup].promise = tx.promise
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const dependencies = request.dependsOn?.map(id => ids[id].promise)
|
|
202
|
+
if (dependencies) {
|
|
203
|
+
// TODO: fail the dependent request if dependency fails
|
|
204
|
+
await Promise.allSettled(dependencies)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
tx.add(request, req, res).then(request => {
|
|
208
|
+
if (separator) res.write(separator)
|
|
209
|
+
else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
|
|
210
|
+
_formatResponse(request, res, boundary)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
if (!atomicityGroup) tx.done()
|
|
214
|
+
|
|
215
|
+
previousAtomicityGroup = atomicityGroup
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (tx) await tx.done()
|
|
219
|
+
|
|
220
|
+
res.write(isJson ? ']}' : `${CRLF}--${boundary}--${CRLF}`)
|
|
221
|
+
res.end()
|
|
222
|
+
|
|
223
|
+
return
|
|
224
|
+
} catch (e) {
|
|
225
|
+
next(e)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* multipart/mixed
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
const _multipartBatch = (srv, router) => async (req, res, next) => {
|
|
234
|
+
const boundary = req.headers['content-type']?.match(/boundary=([\w_-]+)/i)?.[1]
|
|
235
|
+
if (!boundary) return next(cds.error('No boundary found in Content-Type header', { code: 400 }))
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const { requests } = await multipartToJson(req.body, boundary)
|
|
239
|
+
_processBatch(srv, router, req, res, next, { requests }, 'MULTIPART', boundary)
|
|
240
|
+
} catch (e) {
|
|
241
|
+
next(e)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const _formatResponseMultipart = (request, res, boundary) => {
|
|
246
|
+
const { /* id, */ res: response } = request
|
|
247
|
+
|
|
248
|
+
let txt = `--${boundary}${CRLF}content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}${CRLF}`
|
|
249
|
+
txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
|
|
250
|
+
|
|
251
|
+
// REVISIT: tests require specific sequence
|
|
252
|
+
const headers = {
|
|
253
|
+
'odata-version': '4.0',
|
|
254
|
+
'content-type': 'DUMMY',
|
|
255
|
+
...response.getHeaders()
|
|
256
|
+
}
|
|
257
|
+
headers['content-type'] = 'application/json;odata.metadata=minimal' //> REVISIT: expected by tests
|
|
258
|
+
delete headers['content-length'] //> REVISIT: expected by tests
|
|
259
|
+
for (const key in headers) {
|
|
260
|
+
txt += key + ': ' + headers[key] + CRLF
|
|
261
|
+
}
|
|
262
|
+
txt += CRLF
|
|
263
|
+
|
|
264
|
+
if (response._chunk) {
|
|
265
|
+
let _json
|
|
266
|
+
try {
|
|
267
|
+
// REVISIT: tests require specific sequence -> fix and simply use res._chunk
|
|
268
|
+
_json = JSON.parse(response._chunk)
|
|
269
|
+
if (typeof _json !== 'object') throw new Error('not an object')
|
|
270
|
+
let meta = [],
|
|
271
|
+
data = []
|
|
272
|
+
for (const [k, v] of Object.entries(_json)) {
|
|
273
|
+
if (k.startsWith('@')) meta.push(`"${k}":"${v}"`)
|
|
274
|
+
else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
|
|
275
|
+
}
|
|
276
|
+
const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
|
|
277
|
+
txt += _json_as_txt
|
|
278
|
+
} catch {
|
|
279
|
+
// ignore error and take chunk as is (a text)
|
|
280
|
+
txt += response._chunk
|
|
281
|
+
txt = txt.replace('content-type: application/json;odata.metadata=minimal', 'content-type: text/plain')
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
res.write(txt)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/*
|
|
289
|
+
* application/json
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
const _formatStatics = {
|
|
293
|
+
comma: ','.charCodeAt(0),
|
|
294
|
+
body: Buffer.from('"body":'),
|
|
295
|
+
close: Buffer.from('}')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const _formatResponseJson = (request, res) => {
|
|
299
|
+
const { id, res: response } = request
|
|
300
|
+
const raw = Buffer.from(
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
id,
|
|
303
|
+
status: response.statusCode,
|
|
304
|
+
headers: {
|
|
305
|
+
'odata-version': '4.0',
|
|
306
|
+
'content-type': 'application/json',
|
|
307
|
+
...response.getHeaders()
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
// change last "}" into ","
|
|
312
|
+
raw[raw.byteLength - 1] = _formatStatics.comma
|
|
313
|
+
res.write(raw)
|
|
314
|
+
res.write(_formatStatics.body)
|
|
315
|
+
res.write(response._chunk)
|
|
316
|
+
res.write(_formatStatics.close)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const _jsonBatch = (srv, router) => (req, res, next) => {
|
|
320
|
+
_processBatch(srv, router, req, res, next)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = (srv, router) => {
|
|
324
|
+
const handleJsonBatch = _jsonBatch(srv, router)
|
|
325
|
+
const handleMultipartBatch = _multipartBatch(srv, router)
|
|
326
|
+
|
|
327
|
+
return function batch(req, res, next) {
|
|
328
|
+
if (req.headers['content-type'].includes('application/json')) {
|
|
329
|
+
return handleJsonBatch(req, res, next)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (req.headers['content-type'].includes('multipart/mixed')) {
|
|
333
|
+
return express.text({ type: '*/*' })(req, res, () => {
|
|
334
|
+
handleMultipartBatch(req, res, next)
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
throw cds.error('Batch requests must have content type multipart/mixed or application/json', { code: 400 })
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
const { INSERT } = cds.ql
|
|
3
3
|
|
|
4
|
-
const { toODataResult } = require('../utils/result')
|
|
5
|
-
const {
|
|
4
|
+
const { toODataResult, postProcess } = require('../utils/result')
|
|
5
|
+
const { calculateLocationHeader, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
6
6
|
|
|
7
7
|
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
8
8
|
|
|
@@ -10,74 +10,71 @@ const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
|
10
10
|
const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
|
|
11
11
|
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
12
12
|
|
|
13
|
-
const _calculateLocationHeader = (target, srv, result) => {
|
|
14
|
-
const targetName = target.name.replace(`${srv.name}.`, '')
|
|
15
|
-
|
|
16
|
-
const keyValuePairs = Object.keys(target.keys).reduce((acc, key) => {
|
|
17
|
-
acc[key] = result[key]
|
|
18
|
-
return acc
|
|
19
|
-
}, {})
|
|
20
|
-
|
|
21
|
-
let keys
|
|
22
|
-
const entries = Object.entries(keyValuePairs)
|
|
23
|
-
if (entries.length === 1) {
|
|
24
|
-
keys = entries[0][1]
|
|
25
|
-
} else {
|
|
26
|
-
keys = entries.map(([key, value]) => `${key}=${value}`).join(',')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return `${targetName}(${keys})`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
13
|
module.exports = srv =>
|
|
33
14
|
function create(req, res, next) {
|
|
34
|
-
const { _query: query } = req
|
|
35
|
-
|
|
36
15
|
const {
|
|
37
|
-
SELECT: { one, from }
|
|
38
|
-
|
|
16
|
+
SELECT: { one, from },
|
|
17
|
+
target
|
|
18
|
+
} = req._query
|
|
39
19
|
|
|
40
20
|
if (one) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
21
|
+
// REVISIT: don't use "SINGLETON" or "ENTITY" as that are okra terms
|
|
22
|
+
throw Object.assign(
|
|
23
|
+
new Error(`Method ${req.method} not allowed for ${target._isSingleton ? 'SINGLETON' : 'ENTITY'}`),
|
|
24
|
+
{ statusCode: 405 }
|
|
25
|
+
)
|
|
44
26
|
}
|
|
45
27
|
|
|
28
|
+
// payload & params
|
|
46
29
|
const data = deepCopy(req.body)
|
|
47
|
-
|
|
30
|
+
const { keys, params } = getKeysAndParamsFromPath(from, srv)
|
|
48
31
|
// add keys from url into payload (overwriting if already present)
|
|
49
|
-
Object.assign(data,
|
|
32
|
+
Object.assign(data, keys)
|
|
50
33
|
|
|
51
34
|
// assert payload
|
|
52
35
|
const assertOptions = { filter: true, http: { req }, mandatories: true }
|
|
53
|
-
const errs = cds.assert(data,
|
|
36
|
+
const errs = cds.assert(data, target, assertOptions)
|
|
54
37
|
if (errs) {
|
|
55
38
|
if (errs.length === 1) throw errs[0]
|
|
56
39
|
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
57
40
|
}
|
|
58
41
|
|
|
59
|
-
|
|
42
|
+
// query
|
|
43
|
+
const query = INSERT.into(from).entries(data)
|
|
60
44
|
|
|
61
|
-
// we need
|
|
62
|
-
const cdsReq = new cds.Request({ query
|
|
45
|
+
// 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 })
|
|
47
|
+
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
63
48
|
|
|
64
49
|
// rewrite event for draft-enabled entities
|
|
65
|
-
if (
|
|
50
|
+
if (target._isDraftEnabled) cdsReq.event = 'NEW'
|
|
66
51
|
|
|
52
|
+
// REVISIT: only via srv.run in combination with srv.dispatch inside
|
|
53
|
+
// we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
|
|
54
|
+
// or the auto-managed tx opened for the respective atomicity group, if exists
|
|
67
55
|
return srv
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// TODO see if in old odata impl for other checks that should happen
|
|
72
|
-
result = await readAfterWrite(cdsReq, srv, { operation: { result } })
|
|
73
|
-
}
|
|
56
|
+
.run(() => {
|
|
57
|
+
return srv.dispatch(cdsReq).then(result => {
|
|
58
|
+
handleSapMessages(cdsReq, req, res)
|
|
74
59
|
|
|
75
|
-
|
|
60
|
+
if (cdsReq._.readAfterWrite) {
|
|
61
|
+
return readAfterWrite(cdsReq, srv, { operation: { result } })
|
|
62
|
+
}
|
|
76
63
|
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
return result
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
.then(result => {
|
|
68
|
+
// we use an extra then block, after getting the result, so the transaction is commited, before sending the response
|
|
69
|
+
if (result == null) return res.sendStatus(204)
|
|
70
|
+
const isMinimal = req._preferReturn === 'minimal'
|
|
71
|
+
postProcess(cdsReq.target, srv, result, isMinimal)
|
|
72
|
+
if (result['$etag']) res.set('etag', result['$etag'])
|
|
73
|
+
res.set('location', calculateLocationHeader(cdsReq.target, srv, result))
|
|
74
|
+
if (isMinimal) return res.sendStatus(204)
|
|
75
|
+
const info = metaInfo(query, 'CREATE', srv, result, req)
|
|
79
76
|
result = toODataResult(result, info)
|
|
80
|
-
res.set('
|
|
77
|
+
res.status(201).set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
|
|
81
78
|
})
|
|
82
|
-
.catch(next)
|
|
79
|
+
.catch(next) // should be outside, so tx can be rolled back in case of errors
|
|
83
80
|
}
|
|
@@ -1,37 +1,49 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
const { UPDATE, DELETE } = cds.ql
|
|
3
3
|
|
|
4
|
-
const {
|
|
4
|
+
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
5
5
|
|
|
6
6
|
module.exports = srv =>
|
|
7
7
|
function deleete(req, res, next) {
|
|
8
8
|
if (req._preferReturn) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
throw Object.assign(new Error(`The 'return' preference is not allowed in ${req.method} requests`), {
|
|
10
|
+
statusCode: 400
|
|
11
|
+
})
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// REVISIT: better solution for query._propertyAccess
|
|
15
15
|
const {
|
|
16
|
-
SELECT: { one, from }
|
|
17
|
-
|
|
16
|
+
SELECT: { one, from },
|
|
17
|
+
target,
|
|
18
|
+
_propertyAccess
|
|
19
|
+
} = req._query
|
|
18
20
|
|
|
19
21
|
if (!one) {
|
|
20
22
|
// REVISIT: don't use "ENTITY.COLLECTION" as that's an okra term
|
|
21
|
-
|
|
23
|
+
throw Object.assign(new Error('Method DELETE not allowed for ENTITY.COLLECTION'), { statusCode: 405 })
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
+
// payload & params
|
|
27
|
+
const { keys, params } = getKeysAndParamsFromPath(from, srv)
|
|
28
|
+
const data = keys //> for read and delete, we provide keys in req.data
|
|
29
|
+
if (_propertyAccess) data[_propertyAccess] = null //> delete of property -> set to null
|
|
30
|
+
|
|
31
|
+
// query
|
|
32
|
+
const query = _propertyAccess ? UPDATE(from).set({ [_propertyAccess]: null }) : DELETE.from(from)
|
|
26
33
|
|
|
27
|
-
//
|
|
28
|
-
|
|
34
|
+
// 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
|
+
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
37
|
+
|
|
38
|
+
// rewrite event for draft-enabled entities
|
|
39
|
+
if (target._isDraftEnabled && cdsReq.data.IsActiveEntity === false) cdsReq.event = 'CANCEL'
|
|
29
40
|
|
|
30
|
-
// REVISIT: maybe also just dispatch a cds request here?
|
|
31
41
|
return srv
|
|
32
|
-
.
|
|
42
|
+
.dispatch(cdsReq)
|
|
33
43
|
.then(result => {
|
|
34
|
-
|
|
44
|
+
handleSapMessages(cdsReq, req, res)
|
|
45
|
+
|
|
46
|
+
if (result === 0) throw Object.assign(new Error('Not found'), { statusCode: 404 })
|
|
35
47
|
res.sendStatus(204)
|
|
36
48
|
})
|
|
37
49
|
.catch(next)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const { normalizeError } = require('../../_runtime/common/error/frontend')
|
|
2
2
|
|
|
3
|
-
module.exports = _srv =>
|
|
4
|
-
|
|
3
|
+
module.exports = _srv =>
|
|
4
|
+
function error(err, req, res, _next) {
|
|
5
|
+
const { error, statusCode } = normalizeError(err, req)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
7
|
+
// NOTE: normalizeError already does sanatization -> we can use as is
|
|
8
|
+
res.status(statusCode).json({ error })
|
|
9
|
+
}
|
|
@@ -3,8 +3,6 @@ const LOG = cds.log('odata')
|
|
|
3
3
|
|
|
4
4
|
const crypto = require('crypto')
|
|
5
5
|
|
|
6
|
-
const { odataError } = require('../utils')
|
|
7
|
-
|
|
8
6
|
const _requestedFormat = (queryOption, header) => {
|
|
9
7
|
if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
|
|
10
8
|
if (header) {
|
|
@@ -36,9 +34,9 @@ const normalize_header = value => {
|
|
|
36
34
|
return value.split(',').map(str => str.trim())
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
const validate_etag = (
|
|
40
|
-
const
|
|
41
|
-
return
|
|
37
|
+
const validate_etag = (header, etag) => {
|
|
38
|
+
const normalized = normalize_header(header)
|
|
39
|
+
return normalized.includes(etag) || normalized.includes('*') || normalized.includes('"*"')
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
const generateEtag = s => {
|
|
@@ -54,7 +52,7 @@ const mpSupportsEmptyLocale = () => {
|
|
|
54
52
|
module.exports = srv =>
|
|
55
53
|
async function metadata(req, res, _next) {
|
|
56
54
|
if (req.method !== 'GET')
|
|
57
|
-
|
|
55
|
+
throw Object.assign(new Error(`Method ${req.method} not allowed for $metadata.`), { statusCode: 405 })
|
|
58
56
|
|
|
59
57
|
const tenant = cds.context.tenant
|
|
60
58
|
const locale = cds.context.locale
|
|
@@ -66,6 +64,13 @@ module.exports = srv =>
|
|
|
66
64
|
|
|
67
65
|
const etag = format === 'json' ? metadataCache.jsonEtag?.[locale] : metadataCache.xmlEtag?.[locale]
|
|
68
66
|
|
|
67
|
+
if (req.headers['if-match']) {
|
|
68
|
+
if (etag) {
|
|
69
|
+
const valid = validate_etag(req.headers['if-match'], etag)
|
|
70
|
+
if (!valid) return res.status(412).end()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
if (req.headers['if-none-match']) {
|
|
70
75
|
if (etag) {
|
|
71
76
|
const unchanged = validate_etag(req.headers['if-none-match'], etag)
|
|
@@ -79,14 +84,10 @@ module.exports = srv =>
|
|
|
79
84
|
const { 'cds.xt.ModelProviderService': mps } = cds.services
|
|
80
85
|
if (mps) {
|
|
81
86
|
if (format === 'json')
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
'UNSUPPORTED_METADATA_TYPE',
|
|
87
|
-
'JSON metadata is not supported if cds.requires.extensibilty: true.'
|
|
88
|
-
)
|
|
89
|
-
)
|
|
87
|
+
throw Object.assign(new Error('JSON metadata is not supported if cds.requires.extensibilty: true.'), {
|
|
88
|
+
statusCode: 400,
|
|
89
|
+
code: 'UNSUPPORTED_METADATA_TYPE'
|
|
90
|
+
})
|
|
90
91
|
|
|
91
92
|
try {
|
|
92
93
|
let edmx
|
|
@@ -113,7 +114,7 @@ module.exports = srv =>
|
|
|
113
114
|
LOG.error(e)
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
throw Object.assign(new Error('Service Unavailable'), { statusCode: 503 })
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|