@sap/cds 9.6.4 → 9.7.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 +57 -0
- package/bin/serve.js +38 -26
- package/lib/compile/for/flows.js +100 -20
- package/lib/compile/for/lean_drafts.js +0 -47
- package/lib/compile/for/nodejs.js +47 -14
- package/lib/compile/for/odata.js +20 -0
- package/lib/compile/load.js +22 -25
- package/lib/compile/minify.js +29 -11
- package/lib/compile/parse.js +1 -1
- package/lib/compile/resolve.js +133 -76
- package/lib/compile/to/csn.js +2 -2
- package/lib/dbs/cds-deploy.js +48 -43
- package/lib/env/cds-env.js +6 -0
- package/lib/env/cds-requires.js +9 -3
- package/lib/index.js +3 -1
- package/lib/plugins.js +1 -1
- package/lib/req/request.js +2 -2
- package/lib/srv/bindings.js +10 -5
- package/lib/srv/middlewares/auth/index.js +7 -5
- package/lib/srv/protocols/hcql.js +8 -3
- package/lib/srv/protocols/http.js +1 -1
- package/lib/srv/protocols/index.js +1 -0
- package/lib/utils/cds-utils.js +28 -1
- package/lib/utils/colors.js +1 -1
- package/libx/_runtime/common/generic/assert.js +1 -7
- package/libx/_runtime/common/generic/flows.js +14 -4
- package/libx/_runtime/common/utils/resolveView.js +4 -0
- package/libx/_runtime/fiori/lean-draft.js +8 -3
- package/libx/_runtime/messaging/common-utils/authorizedRequest.js +4 -0
- package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +12 -12
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/messaging/http-utils/token.js +18 -3
- package/libx/_runtime/messaging/message-queuing.js +7 -7
- package/libx/_runtime/remote/Service.js +14 -3
- package/libx/_runtime/remote/utils/client.js +1 -0
- package/libx/_runtime/remote/utils/query.js +0 -1
- package/libx/odata/middleware/batch.js +128 -112
- package/libx/odata/middleware/delete.js +2 -1
- package/libx/odata/middleware/error.js +7 -3
- package/libx/odata/parse/afterburner.js +10 -11
- package/libx/odata/parse/grammar.peggy +4 -2
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/odataBind.js +8 -2
- package/libx/queue/index.js +1 -0
- package/package.json +4 -7
- package/srv/outbox.cds +1 -1
- package/srv/ucl-service.cds +3 -5
- package/bin/colors.js +0 -2
- package/libx/_runtime/.eslintrc +0 -14
|
@@ -9,7 +9,7 @@ const { URL } = require('url')
|
|
|
9
9
|
const multipartToJson = require('../parse/multipartToJson')
|
|
10
10
|
const { getBoundary } = require('../utils')
|
|
11
11
|
|
|
12
|
-
const {
|
|
12
|
+
const { odataError } = require('./error')
|
|
13
13
|
|
|
14
14
|
const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
|
|
15
15
|
const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
|
|
@@ -120,6 +120,34 @@ const _validateBatch = body => {
|
|
|
120
120
|
return ids
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/*
|
|
124
|
+
* lookalike
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
let error_mws
|
|
128
|
+
const _getNextForLookalike = lookalike => {
|
|
129
|
+
error_mws ??= cds.middlewares.after.filter(mw => mw.length === 4) // error middleware has 4 params
|
|
130
|
+
return err => {
|
|
131
|
+
let _err = err
|
|
132
|
+
let _next_called
|
|
133
|
+
const _next = e => {
|
|
134
|
+
_err = e
|
|
135
|
+
_next_called = true
|
|
136
|
+
}
|
|
137
|
+
for (const mw of error_mws) {
|
|
138
|
+
_next_called = false
|
|
139
|
+
mw(_err, lookalike.req, lookalike.res, _next)
|
|
140
|
+
if (!_next_called) break //> next chain was interrupted -> done
|
|
141
|
+
}
|
|
142
|
+
if (_next_called) {
|
|
143
|
+
// here, final error middleware called next (which actually shouldn't happen!)
|
|
144
|
+
if (_err.statusCode) lookalike.res.status(_err.statusCode)
|
|
145
|
+
if (typeof _err === 'object') lookalike.res.json({ error: _err })
|
|
146
|
+
else lookalike.res.send(_err)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
123
151
|
// REVISIT: Why not simply use {__proto__:req, ...}?
|
|
124
152
|
const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
125
153
|
const { id, method, url } = request
|
|
@@ -165,6 +193,59 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
165
193
|
return ret
|
|
166
194
|
}
|
|
167
195
|
|
|
196
|
+
/*
|
|
197
|
+
* multipart/mixed response
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
const _formatResponseMultipart = request => {
|
|
201
|
+
const { res: response } = request
|
|
202
|
+
const content_id = request.req?.headers['content-id']
|
|
203
|
+
|
|
204
|
+
let txt = `content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}`
|
|
205
|
+
if (content_id) txt += `content-id: ${content_id}${CRLF}`
|
|
206
|
+
txt += CRLF
|
|
207
|
+
txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
|
|
208
|
+
|
|
209
|
+
// REVISIT: tests require specific sequence
|
|
210
|
+
const headers = {
|
|
211
|
+
...response.getHeaders(),
|
|
212
|
+
...(response.statusCode !== 204 && { 'content-type': 'application/json;odata.metadata=minimal' })
|
|
213
|
+
}
|
|
214
|
+
delete headers['content-length'] //> REVISIT: expected by tests
|
|
215
|
+
|
|
216
|
+
for (const key in headers) {
|
|
217
|
+
txt += key + ': ' + headers[key] + CRLF
|
|
218
|
+
}
|
|
219
|
+
txt += CRLF
|
|
220
|
+
|
|
221
|
+
const _tryParse = x => {
|
|
222
|
+
try {
|
|
223
|
+
return JSON.parse(x)
|
|
224
|
+
} catch {
|
|
225
|
+
return x
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (response._chunk) {
|
|
230
|
+
const chunk = _tryParse(response._chunk)
|
|
231
|
+
if (chunk && typeof chunk === 'object') {
|
|
232
|
+
let meta = [],
|
|
233
|
+
data = []
|
|
234
|
+
for (const [k, v] of Object.entries(chunk)) {
|
|
235
|
+
if (k.startsWith('@')) meta.push(`"${k}":${typeof v === 'string' ? `"${v.replaceAll('"', '\\"')}"` : v}`)
|
|
236
|
+
else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
|
|
237
|
+
}
|
|
238
|
+
const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
|
|
239
|
+
txt += _json_as_txt
|
|
240
|
+
} else {
|
|
241
|
+
txt += chunk
|
|
242
|
+
txt = txt.replace('content-type: application/json;odata.metadata=minimal', 'content-type: text/plain')
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return [txt]
|
|
247
|
+
}
|
|
248
|
+
|
|
168
249
|
const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
|
|
169
250
|
res.write(`--${boundary}${CRLF}`)
|
|
170
251
|
|
|
@@ -185,6 +266,38 @@ const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
|
|
|
185
266
|
}
|
|
186
267
|
}
|
|
187
268
|
|
|
269
|
+
/*
|
|
270
|
+
* application/json response
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
const _formatStatics = {
|
|
274
|
+
comma: ','.charCodeAt(0),
|
|
275
|
+
body: Buffer.from('"body":'),
|
|
276
|
+
close: Buffer.from('}')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const _formatResponseJson = (request, atomicityGroup) => {
|
|
280
|
+
const { id, res: response } = request
|
|
281
|
+
|
|
282
|
+
const chunk = {
|
|
283
|
+
id,
|
|
284
|
+
status: response.statusCode,
|
|
285
|
+
headers: {
|
|
286
|
+
...response.getHeaders(),
|
|
287
|
+
'content-type': 'application/json' //> REVISIT: why?
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (atomicityGroup) chunk.atomicityGroup = atomicityGroup
|
|
291
|
+
const raw = Buffer.from(JSON.stringify(chunk))
|
|
292
|
+
|
|
293
|
+
// body?
|
|
294
|
+
if (!response._chunk) return [raw]
|
|
295
|
+
|
|
296
|
+
// change last "}" into ","
|
|
297
|
+
raw[raw.byteLength - 1] = _formatStatics.comma
|
|
298
|
+
return [raw, _formatStatics.body, response._chunk, _formatStatics.close]
|
|
299
|
+
}
|
|
300
|
+
|
|
188
301
|
const _writeResponseJson = (responses, res) => {
|
|
189
302
|
for (const resp of responses) {
|
|
190
303
|
if (resp.separator) res.write(resp.separator)
|
|
@@ -192,37 +305,25 @@ const _writeResponseJson = (responses, res) => {
|
|
|
192
305
|
}
|
|
193
306
|
}
|
|
194
307
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const _next = e => {
|
|
202
|
-
_err = e
|
|
203
|
-
_next_called = true
|
|
204
|
-
}
|
|
205
|
-
for (const mw of error_mws) {
|
|
206
|
-
_next_called = false
|
|
207
|
-
mw(_err, lookalike.req, lookalike.res, _next)
|
|
208
|
-
if (!_next_called) break //> next chain was interrupted -> done
|
|
209
|
-
}
|
|
210
|
-
if (_next_called) {
|
|
211
|
-
// here, final error middleware called next (which actually shouldn't happen!)
|
|
212
|
-
if (_err.statusCode) lookalike.res.status(_err.statusCode)
|
|
213
|
-
if (typeof _err === 'object') lookalike.res.json({ error: _err })
|
|
214
|
-
else lookalike.res.send(_err)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
308
|
+
/*
|
|
309
|
+
* process
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
const _txs = {}
|
|
313
|
+
cds.once('shutdown', () => Promise.all(Object.values(_txs).map(tx => tx.rollback().catch(() => {}))))
|
|
218
314
|
|
|
219
315
|
// REVISIT: This looks frightening -> need to review
|
|
220
316
|
const _transaction = async srv => {
|
|
221
317
|
return new Promise(res => {
|
|
222
318
|
const ret = {}
|
|
223
319
|
const _tx = (ret._tx = srv.tx(
|
|
224
|
-
async
|
|
320
|
+
async tx =>
|
|
225
321
|
(ret.promise = new Promise((resolve, reject) => {
|
|
322
|
+
// ensure rollback on shutdown so the db can disconnect
|
|
323
|
+
const _id = cds.utils.uuid()
|
|
324
|
+
_txs[_id] = tx
|
|
325
|
+
tx.context.on('done', () => delete _txs[_id])
|
|
326
|
+
|
|
226
327
|
const proms = []
|
|
227
328
|
// It's important to run `makePromise` in the current execution context (cb of srv.tx),
|
|
228
329
|
// otherwise, it will use a different transaction.
|
|
@@ -258,7 +359,7 @@ const _tx_done = async (tx, responses, isJson) => {
|
|
|
258
359
|
// here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
|
|
259
360
|
rejected = 'rejected'
|
|
260
361
|
// construct commit error (without modifying original error)
|
|
261
|
-
const error =
|
|
362
|
+
const error = odataError(Object.create(e), {
|
|
262
363
|
get locale() {
|
|
263
364
|
return cds.context.locale
|
|
264
365
|
},
|
|
@@ -463,7 +564,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
463
564
|
}
|
|
464
565
|
|
|
465
566
|
/*
|
|
466
|
-
*
|
|
567
|
+
* exports
|
|
467
568
|
*/
|
|
468
569
|
|
|
469
570
|
const _multipartBatch = async (srv, router, req, res, next) => {
|
|
@@ -479,91 +580,6 @@ const _multipartBatch = async (srv, router, req, res, next) => {
|
|
|
479
580
|
}
|
|
480
581
|
}
|
|
481
582
|
|
|
482
|
-
const _formatResponseMultipart = request => {
|
|
483
|
-
const { res: response } = request
|
|
484
|
-
const content_id = request.req?.headers['content-id']
|
|
485
|
-
|
|
486
|
-
let txt = `content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}`
|
|
487
|
-
if (content_id) txt += `content-id: ${content_id}${CRLF}`
|
|
488
|
-
txt += CRLF
|
|
489
|
-
txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
|
|
490
|
-
|
|
491
|
-
// REVISIT: tests require specific sequence
|
|
492
|
-
const headers = {
|
|
493
|
-
...response.getHeaders(),
|
|
494
|
-
...(response.statusCode !== 204 && { 'content-type': 'application/json;odata.metadata=minimal' })
|
|
495
|
-
}
|
|
496
|
-
delete headers['content-length'] //> REVISIT: expected by tests
|
|
497
|
-
|
|
498
|
-
for (const key in headers) {
|
|
499
|
-
txt += key + ': ' + headers[key] + CRLF
|
|
500
|
-
}
|
|
501
|
-
txt += CRLF
|
|
502
|
-
|
|
503
|
-
const _tryParse = x => {
|
|
504
|
-
try {
|
|
505
|
-
return JSON.parse(x)
|
|
506
|
-
} catch {
|
|
507
|
-
return x
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (response._chunk) {
|
|
512
|
-
const chunk = _tryParse(response._chunk)
|
|
513
|
-
if (chunk && typeof chunk === 'object') {
|
|
514
|
-
let meta = [],
|
|
515
|
-
data = []
|
|
516
|
-
for (const [k, v] of Object.entries(chunk)) {
|
|
517
|
-
if (k.startsWith('@')) meta.push(`"${k}":${typeof v === 'string' ? `"${v.replaceAll('"', '\\"')}"` : v}`)
|
|
518
|
-
else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
|
|
519
|
-
}
|
|
520
|
-
const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
|
|
521
|
-
txt += _json_as_txt
|
|
522
|
-
} else {
|
|
523
|
-
txt += chunk
|
|
524
|
-
txt = txt.replace('content-type: application/json;odata.metadata=minimal', 'content-type: text/plain')
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return [txt]
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/*
|
|
532
|
-
* application/json
|
|
533
|
-
*/
|
|
534
|
-
|
|
535
|
-
const _formatStatics = {
|
|
536
|
-
comma: ','.charCodeAt(0),
|
|
537
|
-
body: Buffer.from('"body":'),
|
|
538
|
-
close: Buffer.from('}')
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const _formatResponseJson = (request, atomicityGroup) => {
|
|
542
|
-
const { id, res: response } = request
|
|
543
|
-
|
|
544
|
-
const chunk = {
|
|
545
|
-
id,
|
|
546
|
-
status: response.statusCode,
|
|
547
|
-
headers: {
|
|
548
|
-
...response.getHeaders(),
|
|
549
|
-
'content-type': 'application/json' //> REVISIT: why?
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
if (atomicityGroup) chunk.atomicityGroup = atomicityGroup
|
|
553
|
-
const raw = Buffer.from(JSON.stringify(chunk))
|
|
554
|
-
|
|
555
|
-
// body?
|
|
556
|
-
if (!response._chunk) return [raw]
|
|
557
|
-
|
|
558
|
-
// change last "}" into ","
|
|
559
|
-
raw[raw.byteLength - 1] = _formatStatics.comma
|
|
560
|
-
return [raw, _formatStatics.body, response._chunk, _formatStatics.close]
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/*
|
|
564
|
-
* exports
|
|
565
|
-
*/
|
|
566
|
-
|
|
567
583
|
module.exports = adapter => {
|
|
568
584
|
const { router, service } = adapter
|
|
569
585
|
const textBodyParser = express.text({
|
|
@@ -38,7 +38,8 @@ module.exports = adapter => {
|
|
|
38
38
|
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
39
39
|
|
|
40
40
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
41
|
-
|
|
41
|
+
// for UPDATE: data is already provided via query
|
|
42
|
+
const cdsReq = adapter.request4({ query, data: query.DELETE ? data : undefined, headers, params, req, res })
|
|
42
43
|
|
|
43
44
|
// NOTES:
|
|
44
45
|
// - only via srv.run in combination with srv.dispatch inside,
|
|
@@ -4,12 +4,16 @@ const { shutdown_on_uncaught_errors } = cds.env.server
|
|
|
4
4
|
exports = module.exports = () =>
|
|
5
5
|
function odata_error(err, req, res, next) {
|
|
6
6
|
if (exports.pass_through(err)) return next(err)
|
|
7
|
-
|
|
8
|
-
if (err.details) err = _fioritized(err)
|
|
9
|
-
exports.normalizeError(err, req)
|
|
7
|
+
exports.odataError(err, req)
|
|
10
8
|
return next(err)
|
|
11
9
|
}
|
|
12
10
|
|
|
11
|
+
exports.odataError = (err, req) => {
|
|
12
|
+
req._is_odata = true
|
|
13
|
+
if (err.details) err = _fioritized(err)
|
|
14
|
+
return exports.normalizeError(err, req)
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
exports.pass_through = err => {
|
|
14
18
|
if (err == 401 || err.code == 401) return true
|
|
15
19
|
if (shutdown_on_uncaught_errors && !(err.status || err.statusCode) && cds.error.isSystemError(err)) return true
|
|
@@ -14,11 +14,13 @@ const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-
|
|
|
14
14
|
let _isRelevantKey
|
|
15
15
|
|
|
16
16
|
function _getDefinition(definition, name, namespace) {
|
|
17
|
-
|
|
18
|
-
definition
|
|
19
|
-
definition
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const def =
|
|
18
|
+
definition.definitions?.[name] ??
|
|
19
|
+
definition.elements?.[name] ??
|
|
20
|
+
definition.actions?.[name] ??
|
|
21
|
+
definition.actions?.[name.replace(namespace + '.', '')]
|
|
22
|
+
|
|
23
|
+
if (def && !def['@cds.api.ignore']) return def
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function _resolveAliasesInRef(ref, target) {
|
|
@@ -150,7 +152,7 @@ function _convertVal(value, element) {
|
|
|
150
152
|
case 'cds.Integer':
|
|
151
153
|
case 'cds.Int16':
|
|
152
154
|
case 'cds.Int32':
|
|
153
|
-
if (
|
|
155
|
+
if (!/^[+-]?\d+$/.test(value)) {
|
|
154
156
|
const msg = `Element "${element.name}" does not contain a valid Integer`
|
|
155
157
|
cds.error({ status: 400, message: msg })
|
|
156
158
|
}
|
|
@@ -238,7 +240,7 @@ const _getDataFromParams = (params, operation) => {
|
|
|
238
240
|
return acc
|
|
239
241
|
}, {})
|
|
240
242
|
} catch (e) {
|
|
241
|
-
|
|
243
|
+
cds.error(400, `Malformed Parameters`, { stack: e.stack, internal: e.message })
|
|
242
244
|
}
|
|
243
245
|
}
|
|
244
246
|
|
|
@@ -452,10 +454,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
452
454
|
} else if (current.isAssociation) {
|
|
453
455
|
if (current._target._service !== _get_service_of(target)) {
|
|
454
456
|
// not exposed target
|
|
455
|
-
|
|
456
|
-
status: 400,
|
|
457
|
-
message: `Property "${current.name}" does not exist in "${target.name.replace(namespace + '.', '')}"`
|
|
458
|
-
})
|
|
457
|
+
_doesNotExistError(false, current.name, target.name.replace(namespace + '.', ''), current.kind)
|
|
459
458
|
}
|
|
460
459
|
|
|
461
460
|
// > navigation
|
|
@@ -339,13 +339,15 @@
|
|
|
339
339
|
throw err
|
|
340
340
|
}
|
|
341
341
|
if (info.nodes?.length !== 1 || !info.nodes[0].filter) _throw()
|
|
342
|
+
// remove superfluous brackets
|
|
343
|
+
while (info.nodes[0].filter.length === 1 && info.nodes[0].filter[0]?.xpr)
|
|
344
|
+
info.nodes[0].filter = info.nodes[0].filter[0].xpr
|
|
342
345
|
const rIdx = info.nodes[0].filter.findIndex(e => e.ref?.[0] === info.id)
|
|
343
|
-
if (
|
|
346
|
+
if (rIdx === -1) _throw()
|
|
344
347
|
const op = info.nodes[0].filter[rIdx + 1]
|
|
345
348
|
const val = info.nodes[0].filter[rIdx + 2]?.val
|
|
346
349
|
// ignore additional filters (like `IsActiveEntity`?)
|
|
347
350
|
if (op !== '=' || !val) _throw()
|
|
348
|
-
|
|
349
351
|
const endVal = info.distance && direction * info.distance
|
|
350
352
|
const distance = Number.isInteger(endVal) ? endVal : null
|
|
351
353
|
const where = [{ func: 'DistanceTo', args: [{ val }, { val: distance }] }]
|