@sap/cds 8.0.3 → 8.0.4
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 +8 -0
- package/lib/env/cds-requires.js +1 -0
- package/libx/_runtime/common/composition/insert.js +1 -1
- package/libx/_runtime/db/query/read.js +18 -9
- package/libx/_runtime/fiori/lean-draft.js +1 -1
- package/libx/_runtime/messaging/event-broker.js +55 -17
- package/libx/odata/middleware/batch.js +81 -34
- package/libx/odata/middleware/read.js +1 -1
- package/libx/odata/utils/metadata.js +1 -1
- package/libx/rest/middleware/error.js +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 8.0.4 - 2024-07-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Atomicity group handling in `$batch`
|
|
12
|
+
- `$batch` im combination with `commit` hooks
|
|
13
|
+
- `continue-on-error` preference for JSON `$batch`
|
|
14
|
+
|
|
7
15
|
## Version 8.0.3 - 2024-07-12
|
|
8
16
|
|
|
9
17
|
### Added
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -82,7 +82,7 @@ const hasDeepInsert = (model, cqn) => {
|
|
|
82
82
|
const getDeepInsertCQNs = (model, cqn) => {
|
|
83
83
|
const into = (cqn.INSERT.into.ref && cqn.INSERT.into.ref[0]) || cqn.INSERT.into.name || cqn.INSERT.into
|
|
84
84
|
const entityName = ensureNoDraftsSuffix(into)
|
|
85
|
-
const draft = entityName !== into
|
|
85
|
+
const draft = entityName !== into || into.endsWith('.drafts') //lean draft
|
|
86
86
|
const dataEntries = cqn.INSERT.entries ? deepCopy(cqn.INSERT.entries) : []
|
|
87
87
|
const entity = model.definitions[entityName]
|
|
88
88
|
const compositionTree = getCompositionTree({
|
|
@@ -60,17 +60,26 @@ const read = (executeSelectCQN, executeStreamCQN, convertStreams) => (model, dbc
|
|
|
60
60
|
|
|
61
61
|
if (query.SELECT.count) {
|
|
62
62
|
if (query.SELECT.limit) {
|
|
63
|
+
// IMPORTANT: do not change order!
|
|
64
|
+
// 1. create the count query synchronously, because it works on a copy
|
|
65
|
+
// 2. run the result query, duplicate names ( SELECT ID, ID ...) throw an error synchronously
|
|
66
|
+
// 3. run the count query
|
|
67
|
+
// reason is that executeSelectCQN modifies the query
|
|
63
68
|
const countQuery = _createCountQuery(query)
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const resultPromise =
|
|
70
|
+
query.SELECT.limit?.rows?.val === 0
|
|
71
|
+
? Promise.resolve([])
|
|
72
|
+
: executeSelectCQN(model, dbc, query, user, locale, isoTs)
|
|
73
|
+
const countPromise = executeSelectCQN(model, dbc, countQuery, user, locale, isoTs)
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
return Promise.
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
// use allSettled here, so all executions are awaited before we rollback
|
|
76
|
+
return Promise.allSettled([countPromise, resultPromise]).then(results => {
|
|
77
|
+
const rejection = results.find(p => p.status === 'rejected')
|
|
78
|
+
if (rejection) throw rejection.reason
|
|
79
|
+
|
|
80
|
+
const [{ value: countResult }, { value: result }] = results
|
|
81
|
+
return _arrayWithCount(result, countValue(countResult))
|
|
82
|
+
})
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
return executeSelectCQN(model, dbc, query, user, locale, isoTs).then(result =>
|
|
@@ -824,7 +824,7 @@ const Read = {
|
|
|
824
824
|
else ownNewDrafts.push(draft)
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
-
const $count = ownDrafts.length + (isCount ? actives[0]?.$count : actives.$count ?? 0)
|
|
827
|
+
const $count = ownDrafts.length + (isCount ? actives[0]?.$count : (actives.$count ?? 0))
|
|
828
828
|
if (isCount) return { $count }
|
|
829
829
|
|
|
830
830
|
Read.merge(query._target, ownDrafts, [], row => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
2
|
|
|
3
|
+
const normalizeIncomingMessage = require('./common-utils/normalizeIncomingMessage')
|
|
3
4
|
const express = require('express')
|
|
4
5
|
const https = require('https')
|
|
5
6
|
const crypto = require('crypto')
|
|
@@ -141,30 +142,57 @@ class EventBroker extends cds.MessagingService {
|
|
|
141
142
|
|
|
142
143
|
// TODO: What if we're in single tenant variant?
|
|
143
144
|
try {
|
|
144
|
-
const ceSource = `${this.options.credentials.ceSource[0]}/${cds.context.tenant}`
|
|
145
145
|
const hostname = this.options.credentials.eventing.http.x509.url.replace(/^https?:\/\//, '')
|
|
146
|
-
|
|
146
|
+
|
|
147
|
+
// take over and cleanse cloudevents headers
|
|
148
|
+
const headers = { ...(msg.headers ?? {}) }
|
|
149
|
+
|
|
150
|
+
const ceId = headers.id
|
|
151
|
+
delete headers.id
|
|
152
|
+
|
|
153
|
+
const ceSource = headers.source
|
|
154
|
+
delete headers.source
|
|
155
|
+
|
|
156
|
+
const ceType = headers.type
|
|
157
|
+
delete headers.type
|
|
158
|
+
|
|
159
|
+
const ceSpecversion = headers.specversion
|
|
160
|
+
delete headers.specversion
|
|
161
|
+
|
|
162
|
+
// const ceDatacontenttype = headers.datacontenttype // not part of the HTTP API
|
|
163
|
+
delete headers.datacontenttype
|
|
164
|
+
|
|
165
|
+
// const ceTime = headers.time // not part of the HTTP API
|
|
166
|
+
delete headers.time
|
|
167
|
+
|
|
147
168
|
const options = {
|
|
148
169
|
hostname: hostname,
|
|
149
170
|
method: 'POST',
|
|
150
171
|
headers: {
|
|
151
|
-
'ce-id':
|
|
172
|
+
'ce-id': ceId,
|
|
152
173
|
'ce-source': ceSource,
|
|
153
|
-
'ce-type':
|
|
154
|
-
'ce-specversion':
|
|
155
|
-
'Content-Type': 'application/json'
|
|
174
|
+
'ce-type': ceType,
|
|
175
|
+
'ce-specversion': ceSpecversion,
|
|
176
|
+
'Content-Type': 'application/json' // because of { data, ...headers } format
|
|
156
177
|
},
|
|
157
178
|
agent: this.agent
|
|
158
179
|
}
|
|
159
180
|
this.LOG.debug('HTTP headers:', JSON.stringify(options.headers))
|
|
160
181
|
this.LOG.debug('HTTP body:', JSON.stringify(msg.data))
|
|
161
|
-
|
|
182
|
+
// what about headers?
|
|
183
|
+
// TODO: Clarify if we should send `{ data, ...headers }` vs. `data` + HTTP headers (`ce-*`)
|
|
184
|
+
await request(options, { data: msg.data, ...headers }) // TODO: fetch does not work with mTLS as of today, requires another module. see https://github.com/nodejs/node/issues/48977
|
|
162
185
|
if (this.LOG._info) this.LOG.info('Emit', { topic: msg.event })
|
|
163
186
|
} catch (e) {
|
|
164
187
|
this.LOG.error('Emit failed:', e.message)
|
|
165
188
|
}
|
|
166
189
|
}
|
|
167
190
|
|
|
191
|
+
prepareHeaders(headers, event) {
|
|
192
|
+
if (!('source' in headers)) headers.source = `${this.options.credentials.ceSource[0]}/${cds.context.tenant}`
|
|
193
|
+
super.prepareHeaders(headers, event)
|
|
194
|
+
}
|
|
195
|
+
|
|
168
196
|
async registerWebhookEndpoints() {
|
|
169
197
|
const webhookBasePath = this.options.webhookPath || '/-/cds/event-broker/webhook'
|
|
170
198
|
cds.app.post(webhookBasePath, _validateCertificate.bind(this))
|
|
@@ -176,17 +204,27 @@ class EventBroker extends cds.MessagingService {
|
|
|
176
204
|
try {
|
|
177
205
|
const event = req.headers['ce-type'] // TG27: type contains namespace, so there's no collision
|
|
178
206
|
const tenant = req.headers['ce-sapconsumertenant']
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
headers: req.headers
|
|
207
|
+
|
|
208
|
+
// take over cloudevents headers (`ce-*`) without the prefix
|
|
209
|
+
const headers = {}
|
|
210
|
+
for (const header in req.headers) {
|
|
211
|
+
if (header.startsWith('ce-')) headers[header.slice(3)] = req.headers[header]
|
|
185
212
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
|
|
214
|
+
const msg = normalizeIncomingMessage(req.body)
|
|
215
|
+
msg.event = event
|
|
216
|
+
Object.assign(msg.headers, headers)
|
|
217
|
+
if (tenant) msg.tenant = tenant
|
|
218
|
+
|
|
219
|
+
// for cds.context.http
|
|
220
|
+
msg._ = {}
|
|
221
|
+
msg._.req = req
|
|
222
|
+
msg._.res = res
|
|
223
|
+
|
|
224
|
+
const context = { user: cds.User.privileged, _: msg._ }
|
|
225
|
+
if (msg.tenant) context.tenant = msg.tenant
|
|
226
|
+
|
|
227
|
+
await this.tx(context, tx => tx.emit(msg))
|
|
190
228
|
this.LOG.debug('Event processed successfully.')
|
|
191
229
|
return res.status(200).json({ message: 'OK' })
|
|
192
230
|
} catch (e) {
|
|
@@ -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]
|
|
@@ -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
|
|
@@ -35,7 +35,7 @@ const _odataContext = (query, options) => {
|
|
|
35
35
|
path = '../'.repeat(ref.length - 1) + path
|
|
36
36
|
}
|
|
37
37
|
const lastRef = ref.at(-1)
|
|
38
|
-
let entityName = isNavToDraftAdmin ? ref[0].id ?? ref[0] : query._target.name ?? edmName
|
|
38
|
+
let entityName = isNavToDraftAdmin ? (ref[0].id ?? ref[0]) : (query._target.name ?? edmName)
|
|
39
39
|
const serviceName = query._target._service?.name
|
|
40
40
|
|
|
41
41
|
if (query._target._isContained) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
1
|
const cds = require('../../_runtime/cds')
|
|
4
2
|
|
|
3
|
+
/*
|
|
4
|
+
|
|
5
5
|
// requesting logger without module on purpose!
|
|
6
6
|
const LOG = cds.log()
|
|
7
7
|
|
|
@@ -44,7 +44,14 @@ const _log = require('../../_runtime/common/error/log')
|
|
|
44
44
|
|
|
45
45
|
const { normalizeError } = require('../../_runtime/common/error/frontend')
|
|
46
46
|
|
|
47
|
+
function noop_error_middleware(err, req, res, next) {
|
|
48
|
+
next(err)
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
module.exports = () => {
|
|
52
|
+
// REVISIT: unofficial hack for afc!!!
|
|
53
|
+
if (cds.env.features.rest_error_handler === false) return noop_error_middleware
|
|
54
|
+
|
|
48
55
|
return function rest_error(err, req, res, next) {
|
|
49
56
|
if (err == 401 || err.code == 401) return next(err) // speed up logins, at least temporary until we reviewed and eliminated overhead that may be involved below
|
|
50
57
|
// REVISIT: keep?
|