@sap/cds 8.0.3 → 8.1.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 +40 -0
- package/_i18n/i18n_bg.properties +113 -0
- package/_i18n/i18n_el.properties +113 -0
- package/_i18n/i18n_he.properties +113 -0
- package/_i18n/i18n_hr.properties +113 -0
- package/_i18n/i18n_kk.properties +113 -0
- package/_i18n/i18n_sh.properties +113 -0
- package/_i18n/i18n_sk.properties +113 -0
- package/_i18n/i18n_sl.properties +113 -0
- package/_i18n/i18n_uk.properties +113 -0
- package/lib/compile/etc/_localized.js +8 -20
- package/lib/dbs/cds-deploy.js +1 -0
- package/lib/env/cds-requires.js +1 -0
- package/lib/env/defaults.js +1 -1
- package/lib/env/plugins.js +22 -6
- package/lib/linked/validate.js +1 -1
- package/lib/log/cds-log.js +2 -2
- package/lib/srv/protocols/hcql.js +5 -5
- package/lib/srv/protocols/http.js +23 -11
- package/lib/test/expect.js +1 -1
- package/lib/utils/cds-test.js +4 -4
- package/libx/_runtime/common/composition/insert.js +1 -1
- package/libx/_runtime/common/error/utils.js +2 -1
- package/libx/_runtime/common/generic/input.js +2 -5
- package/libx/_runtime/common/generic/stream.js +18 -3
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -2
- package/libx/_runtime/db/query/read.js +18 -9
- package/libx/_runtime/fiori/lean-draft.js +2 -2
- package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/event-broker.js +76 -24
- package/libx/common/assert/utils.js +1 -57
- package/libx/odata/middleware/batch.js +86 -40
- package/libx/odata/middleware/body-parser.js +2 -3
- package/libx/odata/middleware/operation.js +11 -11
- package/libx/odata/middleware/read.js +1 -1
- package/libx/odata/parse/grammar.peggy +6 -1
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/metadata.js +18 -44
- package/libx/rest/middleware/error.js +9 -2
- package/libx/rest/middleware/parse.js +1 -1
- package/package.json +1 -1
- package/libx/common/assert/index.js +0 -228
- package/libx/common/assert/type-relaxed.js +0 -39
|
@@ -13,8 +13,6 @@ const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
|
|
|
13
13
|
const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
|
|
14
14
|
const CRLF = '\r\n'
|
|
15
15
|
|
|
16
|
-
const CONTINUE_ON_ERROR = /odata\.continue-on-error/
|
|
17
|
-
|
|
18
16
|
/*
|
|
19
17
|
* common
|
|
20
18
|
*/
|
|
@@ -99,16 +97,17 @@ const _validateBatch = body => {
|
|
|
99
97
|
_validateProperty('dependent request ID', dependsOnId, 'string')
|
|
100
98
|
|
|
101
99
|
const dependency = ids[dependsOnId]
|
|
102
|
-
if (!dependency)
|
|
103
|
-
throw _deserializationError(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
request.dependsOn.push(
|
|
100
|
+
if (!dependency) {
|
|
101
|
+
throw _deserializationError(
|
|
102
|
+
`"${dependsOnId}" does not match the id or atomicity group of any preceding request`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// automatically add the atomicityGroup of the dependency as a dependency (actually a client error)
|
|
107
|
+
const dag = dependency.atomicityGroup
|
|
108
|
+
if (dag && dag !== atomicityGroup && !request.dependsOn.includes(dag)) {
|
|
109
|
+
request.dependsOn.push(dag)
|
|
110
|
+
}
|
|
112
111
|
})
|
|
113
112
|
}
|
|
114
113
|
|
|
@@ -227,7 +226,7 @@ const _getNextForLookalike = lookalike => {
|
|
|
227
226
|
const _transaction = async srv => {
|
|
228
227
|
return new Promise(res => {
|
|
229
228
|
const ret = {}
|
|
230
|
-
const _tx = srv.tx(
|
|
229
|
+
const _tx = (ret._tx = srv.tx(
|
|
231
230
|
async () =>
|
|
232
231
|
(ret.promise = new Promise((resolve, reject) => {
|
|
233
232
|
const proms = []
|
|
@@ -253,10 +252,49 @@ const _transaction = async srv => {
|
|
|
253
252
|
}
|
|
254
253
|
res(ret)
|
|
255
254
|
}))
|
|
256
|
-
)
|
|
255
|
+
))
|
|
257
256
|
})
|
|
258
257
|
}
|
|
259
258
|
|
|
259
|
+
const _tx_done = async (tx, responses, isJson) => {
|
|
260
|
+
let rejected
|
|
261
|
+
try {
|
|
262
|
+
rejected = await tx.done()
|
|
263
|
+
} catch (e) {
|
|
264
|
+
// here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
|
|
265
|
+
rejected = 'rejected'
|
|
266
|
+
// construct commit error
|
|
267
|
+
let statusCode = e.statusCode || e.status || (e.code && Number(e.code))
|
|
268
|
+
if (isNaN(statusCode)) statusCode = 500
|
|
269
|
+
const code = String(e.code || statusCode)
|
|
270
|
+
const message = e.message || 'Internal Server Error'
|
|
271
|
+
const error = { error: { ...e, code, message } }
|
|
272
|
+
// replace all responses with commit error
|
|
273
|
+
for (const res of responses) {
|
|
274
|
+
res.status = 'fail'
|
|
275
|
+
// REVISIT: should error go through any error middleware/ customization logic?
|
|
276
|
+
if (isJson) {
|
|
277
|
+
let txt = ''
|
|
278
|
+
for (let i = 0; i < res.txt.length; i++) txt += Buffer.isBuffer(res.txt[i]) ? res.txt[i].toString() : res.txt[i]
|
|
279
|
+
txt = JSON.parse(txt)
|
|
280
|
+
txt.status = statusCode
|
|
281
|
+
txt.body = error
|
|
282
|
+
// REVISIT: content-length needed? not there in multipart case...
|
|
283
|
+
delete txt.headers['content-length']
|
|
284
|
+
res.txt = [JSON.stringify(txt)]
|
|
285
|
+
} else {
|
|
286
|
+
let txt = res.txt[0]
|
|
287
|
+
txt = txt.replace(/HTTP\/1\.1 \d\d\d \w+/, `HTTP/1.1 ${statusCode} ${message}`)
|
|
288
|
+
txt = txt.split(/\r\n/)
|
|
289
|
+
txt.splice(-1, 1, JSON.stringify(error))
|
|
290
|
+
txt = txt.join('\r\n')
|
|
291
|
+
res.txt = [txt]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return rejected
|
|
296
|
+
}
|
|
297
|
+
|
|
260
298
|
const _processBatch = async (srv, router, req, res, next, body, ct, boundary) => {
|
|
261
299
|
body ??= req.body
|
|
262
300
|
ct ??= 'JSON'
|
|
@@ -268,6 +306,14 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
268
306
|
}
|
|
269
307
|
const _formatResponse = isJson ? _formatResponseJson : _formatResponseMultipart
|
|
270
308
|
|
|
309
|
+
// continue-on-error defaults to true in json batch
|
|
310
|
+
let continue_on_error = req.headers.prefer?.match(/odata\.continue-on-error(=(\w+))?/)
|
|
311
|
+
if (!continue_on_error) {
|
|
312
|
+
continue_on_error = isJson ? true : false
|
|
313
|
+
} else {
|
|
314
|
+
continue_on_error = continue_on_error[2] === 'false' ? false : true
|
|
315
|
+
}
|
|
316
|
+
|
|
271
317
|
try {
|
|
272
318
|
const ids = _validateBatch(body) // REVISIT: we will not be able to validate the whole once we stream
|
|
273
319
|
|
|
@@ -304,13 +350,13 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
304
350
|
if (tx) {
|
|
305
351
|
// Each change in `atomicityGroup` results in a new transaction. We execute them in sequence to avoid too many database connections.
|
|
306
352
|
// In the future, we might make this configurable (e.g. allow X parallel connections per HTTP request).
|
|
307
|
-
const rejected = await tx
|
|
353
|
+
const rejected = await _tx_done(tx, responses, isJson)
|
|
308
354
|
if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
|
|
309
355
|
else sendPreludeOnce()
|
|
310
356
|
isJson
|
|
311
357
|
? _writeResponseJson(responses, res)
|
|
312
358
|
: _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
|
|
313
|
-
if (rejected && !
|
|
359
|
+
if (rejected && !continue_on_error) {
|
|
314
360
|
tx = null
|
|
315
361
|
break
|
|
316
362
|
}
|
|
@@ -318,7 +364,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
318
364
|
|
|
319
365
|
responses = []
|
|
320
366
|
tx = await _transaction(srv)
|
|
321
|
-
if (atomicityGroup) ids[atomicityGroup].promise = tx.
|
|
367
|
+
if (atomicityGroup) ids[atomicityGroup].promise = tx._tx
|
|
322
368
|
}
|
|
323
369
|
|
|
324
370
|
tx.add(() => {
|
|
@@ -329,12 +375,13 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
329
375
|
const results = await Promise.allSettled(dependencies)
|
|
330
376
|
const dependendOnFailed = results.some(({ status }) => status === 'rejected')
|
|
331
377
|
if (dependendOnFailed) {
|
|
378
|
+
tx.id = request.id
|
|
332
379
|
tx.res = {
|
|
333
380
|
getHeaders: () => {},
|
|
334
|
-
statusCode:
|
|
381
|
+
statusCode: 424,
|
|
335
382
|
_chunk: JSON.stringify({
|
|
336
|
-
|
|
337
|
-
message:
|
|
383
|
+
code: '424',
|
|
384
|
+
message: 'Failed Dependency'
|
|
338
385
|
})
|
|
339
386
|
}
|
|
340
387
|
throw tx
|
|
@@ -375,14 +422,14 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
375
422
|
const resp = { status: 'ok' }
|
|
376
423
|
if (separator) resp.separator = separator
|
|
377
424
|
else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
|
|
378
|
-
resp.txt = _formatResponse(req,
|
|
425
|
+
resp.txt = _formatResponse(req, atomicityGroup)
|
|
379
426
|
responses.push(resp)
|
|
380
427
|
})
|
|
381
428
|
.catch(failedReq => {
|
|
382
429
|
const resp = { status: 'fail' }
|
|
383
430
|
if (separator) resp.separator = separator
|
|
384
431
|
else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
|
|
385
|
-
resp.txt = _formatResponse(failedReq,
|
|
432
|
+
resp.txt = _formatResponse(failedReq, atomicityGroup)
|
|
386
433
|
tx.failed = failedReq
|
|
387
434
|
responses.push(resp)
|
|
388
435
|
})
|
|
@@ -392,7 +439,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
392
439
|
|
|
393
440
|
if (tx) {
|
|
394
441
|
// The last open transaction must be finished
|
|
395
|
-
const rejected = await tx
|
|
442
|
+
const rejected = await _tx_done(tx, responses, isJson)
|
|
396
443
|
if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
|
|
397
444
|
else sendPreludeOnce()
|
|
398
445
|
isJson
|
|
@@ -479,19 +526,19 @@ const _formatStatics = {
|
|
|
479
526
|
close: Buffer.from('}')
|
|
480
527
|
}
|
|
481
528
|
|
|
482
|
-
const _formatResponseJson = request => {
|
|
529
|
+
const _formatResponseJson = (request, atomicityGroup) => {
|
|
483
530
|
const { id, res: response } = request
|
|
484
531
|
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
)
|
|
532
|
+
const chunk = {
|
|
533
|
+
id,
|
|
534
|
+
status: response.statusCode,
|
|
535
|
+
headers: {
|
|
536
|
+
...response.getHeaders(),
|
|
537
|
+
'content-type': 'application/json' //> REVISIT: why?
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (atomicityGroup) chunk.atomicityGroup = atomicityGroup
|
|
541
|
+
const raw = Buffer.from(JSON.stringify(chunk))
|
|
495
542
|
|
|
496
543
|
// body?
|
|
497
544
|
if (!response._chunk) return [raw]
|
|
@@ -506,12 +553,11 @@ const _formatResponseJson = request => {
|
|
|
506
553
|
*/
|
|
507
554
|
|
|
508
555
|
module.exports = adapter => {
|
|
509
|
-
const {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const textBodyParser = express.text(options)
|
|
556
|
+
const { router, service } = adapter
|
|
557
|
+
const textBodyParser = express.text({
|
|
558
|
+
...adapter.body_parser_options,
|
|
559
|
+
type: '*/*' // REVISIT: why do we need to override type here?
|
|
560
|
+
})
|
|
515
561
|
|
|
516
562
|
return function odata_batch(req, res, next) {
|
|
517
563
|
if (req.headers['content-type'].includes('application/json')) {
|
|
@@ -3,9 +3,8 @@ const express = require('express')
|
|
|
3
3
|
// basically express.json() with string representation of body stored in req._raw for recovery
|
|
4
4
|
// REVISIT: why do we need our own body parser? Only because of req._raw?
|
|
5
5
|
module.exports = function bodyParser4(adapter, options = {}) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (!options.limit && max_content_length) options.limit = max_content_length
|
|
6
|
+
Object.assign(options, adapter.body_parser_options)
|
|
7
|
+
options.type ??= 'json' // REVISIT: why do we need to override type here?
|
|
9
8
|
const textParser = express.text(options)
|
|
10
9
|
return function http_body_parser(req, res, next) {
|
|
11
10
|
if (typeof req.body === 'object') {
|
|
@@ -51,9 +51,6 @@ module.exports = adapter => {
|
|
|
51
51
|
let { operation, args } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
|
|
52
52
|
if (!operation) return next() //> create or read
|
|
53
53
|
|
|
54
|
-
// REVISIT: should not be necessary
|
|
55
|
-
const _originalQuery = JSON.parse(JSON.stringify(req._query))
|
|
56
|
-
|
|
57
54
|
// unbound vs. bound
|
|
58
55
|
let entity, params
|
|
59
56
|
if (service.model.definitions[operation]) {
|
|
@@ -102,7 +99,6 @@ module.exports = adapter => {
|
|
|
102
99
|
|
|
103
100
|
if (operation.returns._type?.match?.(/^cds\./)) {
|
|
104
101
|
const context = `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`
|
|
105
|
-
|
|
106
102
|
result = { '@odata.context': context, value: result }
|
|
107
103
|
return res.send(result)
|
|
108
104
|
}
|
|
@@ -121,15 +117,19 @@ module.exports = adapter => {
|
|
|
121
117
|
if (operation.returns.type !== 'sap.esh.SearchResult') {
|
|
122
118
|
const isCollection = !!operation.returns.items
|
|
123
119
|
const _target = operation.returns.items ?? operation.returns
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
const options = { result, isCollection }
|
|
121
|
+
if (!_target.name) {
|
|
122
|
+
// case: return inline type def
|
|
123
|
+
options.edmName = _opResultName({ service, operation, returnType: _target })
|
|
124
|
+
}
|
|
125
|
+
const SELECT = {
|
|
126
|
+
from: query ? { ref: [...query.SELECT.from.ref, { operation: operation.name }] } : {},
|
|
127
|
+
one: !isCollection
|
|
128
|
+
}
|
|
129
|
+
const metadata = getODataMetadata({ SELECT, _target }, options)
|
|
131
130
|
result = getODataResult(result, metadata, { isCollection })
|
|
132
131
|
}
|
|
132
|
+
|
|
133
133
|
res.send(result)
|
|
134
134
|
})
|
|
135
135
|
.catch(err => {
|
|
@@ -109,7 +109,7 @@ const _count = result => {
|
|
|
109
109
|
? result.reduce((acc, val) => {
|
|
110
110
|
return acc + (val?.$count ?? val?._counted_ ?? (Array.isArray(val) && _count(val))) || 0
|
|
111
111
|
}, 0)
|
|
112
|
-
: result.$count ?? result._counted_ ?? 0
|
|
112
|
+
: (result.$count ?? result._counted_ ?? 0)
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// REVISIT: integrate with default handler
|
|
@@ -402,6 +402,7 @@
|
|
|
402
402
|
"$skip=" o val:skip { _setLimitOffset(val) } /
|
|
403
403
|
"$search=" o s:search_expand { if (s) SELECT.search = s } /
|
|
404
404
|
"$count=" o count /
|
|
405
|
+
"$apply=" &{ return cds.env.features.skip_apply_parsing } o [^&]* { return null } /
|
|
405
406
|
"$apply=" o trafos:transformations { return trafos } /
|
|
406
407
|
// Workaround to support empty expand even if not OData compliant old adapter supported it and did not crash
|
|
407
408
|
"$expand=" {return null}
|
|
@@ -486,7 +487,11 @@
|
|
|
486
487
|
/ o // Do not add search property for space only
|
|
487
488
|
|
|
488
489
|
search_clause
|
|
489
|
-
= val:$(
|
|
490
|
+
= val:$(($[ ]* (
|
|
491
|
+
[^"&]+
|
|
492
|
+
/ ('"' ("\\\\" / "\\\"" / [^"])+ '"' [ ]*)+
|
|
493
|
+
))+)
|
|
494
|
+
{ return [{ val }] }
|
|
490
495
|
|
|
491
496
|
search_expand
|
|
492
497
|
= val:$( [^;)]+ ) { return [{ val }] }
|