@sap/cds 9.1.0 → 9.2.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 +53 -0
- package/bin/deploy.js +29 -0
- package/bin/serve.js +1 -5
- package/lib/compile/etc/csv.js +11 -6
- package/lib/compile/load.js +8 -5
- package/lib/compile/to/hdbtabledata.js +1 -1
- package/lib/dbs/cds-deploy.js +0 -31
- package/lib/env/cds-env.js +2 -1
- package/lib/env/cds-requires.js +3 -0
- package/lib/env/schemas/cds-rc.js +4 -0
- package/lib/index.js +38 -38
- package/lib/log/cds-error.js +12 -11
- package/lib/log/format/json.js +1 -1
- package/lib/ql/SELECT.js +31 -0
- package/lib/ql/UPDATE.js +3 -1
- package/lib/ql/resolve.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/req/validate.js +16 -17
- package/lib/srv/cds.Service.js +18 -28
- package/lib/srv/middlewares/auth/ias-auth.js +31 -4
- package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
- package/lib/srv/srv-models.js +1 -1
- package/lib/srv/srv-tx.js +2 -2
- package/lib/utils/cds-utils.js +35 -2
- package/lib/utils/csv-reader.js +1 -1
- package/lib/utils/version.js +18 -0
- package/libx/_runtime/cds.js +1 -1
- package/libx/_runtime/common/aspects/any.js +1 -23
- package/libx/_runtime/common/generic/input.js +111 -50
- package/libx/_runtime/common/generic/sorting.js +1 -1
- package/libx/_runtime/common/utils/draft.js +1 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
- package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -2
- package/libx/_runtime/common/utils/structured.js +2 -2
- package/libx/_runtime/common/utils/templateProcessor.js +0 -5
- package/libx/_runtime/common/utils/vcap.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +63 -23
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
- package/libx/_runtime/messaging/file-based.js +2 -1
- package/libx/_runtime/messaging/service.js +1 -1
- package/libx/_runtime/remote/utils/client.js +1 -1
- package/libx/common/assert/utils.js +2 -12
- package/libx/common/utils/streaming.js +4 -9
- package/libx/http/location.js +1 -0
- package/libx/odata/index.js +1 -1
- package/libx/odata/middleware/batch.js +6 -1
- package/libx/odata/middleware/create.js +1 -1
- package/libx/odata/middleware/error.js +22 -19
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/parse/cqn2odata.js +16 -10
- package/libx/odata/parse/grammar.peggy +8 -4
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +1 -1
- package/libx/queue/index.js +3 -3
- package/libx/rest/RestAdapter.js +1 -2
- package/libx/rest/middleware/create.js +5 -2
- package/package.json +2 -2
- package/server.js +1 -1
- package/bin/deploy/to-hana.js +0 -1
- package/lib/utils/check-version.js +0 -9
- package/lib/utils/unit.js +0 -19
- package/libx/_runtime/cds-services/util/assert.js +0 -181
- package/libx/_runtime/types/api.js +0 -129
- package/libx/common/assert/validation.js +0 -109
|
@@ -24,9 +24,9 @@ const _config_to_ms = (config, _default) => {
|
|
|
24
24
|
const timeout = cds.env.fiori?.[config]
|
|
25
25
|
let timeout_ms
|
|
26
26
|
if (timeout === true) {
|
|
27
|
-
timeout_ms = cds.utils.
|
|
27
|
+
timeout_ms = cds.utils.ms4(_default)
|
|
28
28
|
} else if (typeof timeout === 'string') {
|
|
29
|
-
timeout_ms = cds.utils.
|
|
29
|
+
timeout_ms = cds.utils.ms4(timeout)
|
|
30
30
|
if (!timeout_ms)
|
|
31
31
|
throw new Error(`
|
|
32
32
|
${timeout} is an invalid value for \`cds.fiori.${config}\`.
|
|
@@ -384,26 +384,29 @@ const _replaceStreams = result => {
|
|
|
384
384
|
|
|
385
385
|
const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestData, draftsRef) => {
|
|
386
386
|
const prefixRef = []
|
|
387
|
-
const simplePathElements = []
|
|
388
|
-
let targetEntity = cds.infer.ref([draftsRef[0]], cds.context.tx.model)
|
|
389
387
|
|
|
390
388
|
// Determine the path prefix required for a fully qualified validation message 'target'
|
|
389
|
+
let targetEntity = cds.context.tx.model.definitions[draftsRef[0].id || draftsRef[0]]
|
|
391
390
|
for (let refIdx = 0; refIdx < draftsRef.length; refIdx++) {
|
|
392
|
-
|
|
393
|
-
|
|
391
|
+
const dRef = draftsRef[refIdx],
|
|
392
|
+
dRefId = dRef.id ?? dRef
|
|
394
393
|
|
|
394
|
+
// Determine entity, referenced by the processesd segment of 'draftsRef'
|
|
395
|
+
if (refIdx > 0) targetEntity = targetEntity.elements[dRefId]._target
|
|
396
|
+
|
|
397
|
+
// Construct 'prefixRef' segment
|
|
395
398
|
const pRef = { where: [] }
|
|
396
399
|
|
|
397
|
-
const dRefId = dRef.id ?? dRef
|
|
398
400
|
if (dRefId === targetEntity.name) {
|
|
399
401
|
if (!targetEntity.isDraft) pRef.id = targetEntity.name.replace(`${targetEntity._service.name}.`, '')
|
|
400
402
|
else pRef.id = targetEntity.actives.name.replace(`${targetEntity.actives._service.name}.`, '')
|
|
401
403
|
} else pRef.id = dRefId
|
|
402
|
-
simplePathElements.push(pRef.id)
|
|
403
404
|
|
|
404
405
|
if (targetEntity.isDraft) targetEntity = targetEntity.actives
|
|
405
406
|
|
|
406
407
|
if (typeof dRef === 'string' || !dRef.where?.length) {
|
|
408
|
+
// In case of CREATE: The WHERE for the created entity must be constructed for use in 'prefixRef'
|
|
409
|
+
|
|
407
410
|
for (const k in targetEntity.keys) {
|
|
408
411
|
const key = targetEntity.keys[k]
|
|
409
412
|
let v = requestData[key.name]
|
|
@@ -414,6 +417,7 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
|
|
|
414
417
|
pRef.where.push(key.name, '=', v)
|
|
415
418
|
}
|
|
416
419
|
} else {
|
|
420
|
+
// 'dRef.where' regards the draft and will never contain 'IsActiveEntity=false'
|
|
417
421
|
pRef.where.push(...dRef.where, 'and', 'IsActiveEntity', '=', false)
|
|
418
422
|
}
|
|
419
423
|
|
|
@@ -424,23 +428,30 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
|
|
|
424
428
|
|
|
425
429
|
// Collect messages that were created during the most recent validation run
|
|
426
430
|
const newMessagesByCodeAndTarget = newMessages.reduce((acc, message) => {
|
|
427
|
-
message.
|
|
431
|
+
message.numericSeverity ??= 4
|
|
428
432
|
|
|
429
|
-
|
|
433
|
+
// Handle validation messages produced during draftActivate, that went through error normalization already
|
|
434
|
+
// > We must not store pre-localized data in DraftAdministrativeData.DraftMessages
|
|
430
435
|
const messageTarget = message.target.startsWith('in/') ? message.target.slice(3) : message.target
|
|
436
|
+
if (message.code) {
|
|
437
|
+
message.message = message.code
|
|
438
|
+
delete message.code
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Process the message target produced by validation
|
|
442
|
+
// > The message target contains the relative path of the erroneous entity
|
|
443
|
+
// > For a message specific 'prefixRef', this info must be added to the entity 'prefixRef'
|
|
444
|
+
const messagePrefixRef = [...prefixRef]
|
|
431
445
|
const messageTargetRef = cds.odata.parse(messageTarget).SELECT.from.ref
|
|
432
446
|
message.target = messageTargetRef.pop()
|
|
433
447
|
|
|
434
448
|
for (const tRef of messageTargetRef) {
|
|
435
|
-
message.simplePathElements.push(tRef.id)
|
|
436
449
|
messagePrefixRef.push(tRef)
|
|
437
450
|
if ((tRef.where ??= []).some(w => w === 'IsActiveEntity' || w.ref?.[0] === 'IsActiveEntity')) continue
|
|
438
|
-
if (tRef.where.length > 0) tRef.where.push('and')
|
|
439
|
-
tRef.where.push('IsActiveEntity', '=', false)
|
|
451
|
+
if (tRef.where.length > 0) tRef.where.push('and', 'IsActiveEntity', '=', false)
|
|
440
452
|
}
|
|
441
453
|
|
|
442
454
|
message.prefix = cds.odata.urlify({ SELECT: { from: { ref: messagePrefixRef } } }).path
|
|
443
|
-
message.numericSeverity ??= 4
|
|
444
455
|
|
|
445
456
|
nextMessages.push(message)
|
|
446
457
|
|
|
@@ -450,14 +461,18 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
|
|
|
450
461
|
const draftMessageTargetPrefix = cds.odata.urlify({ SELECT: { from: { ref: prefixRef } } }).path
|
|
451
462
|
|
|
452
463
|
// Merge new messages with persisted ones & eliminate outdated ones
|
|
464
|
+
const simplePathElements = prefixRef.map(pRef => pRef.id)
|
|
453
465
|
for (const message of persistedMessages) {
|
|
466
|
+
// Drop persisted draft messages that are replaced by new ones
|
|
454
467
|
if (newMessagesByCodeAndTarget.has(`${message.message}:${message.prefix}:${message.target}`)) continue
|
|
468
|
+
|
|
469
|
+
// Drop persisted draft messages where the value of the target field changed without a new error
|
|
455
470
|
if (message.prefix === draftMessageTargetPrefix && requestData[message.target] !== undefined) continue
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
)
|
|
460
|
-
continue
|
|
471
|
+
|
|
472
|
+
// Drop persisted draft messages, whose target's use navigations, where the value of the target field changed without a new error
|
|
473
|
+
const messageSimplePathElements = message.prefix.replaceAll(/\([^(]*\)/g, '').split('/')
|
|
474
|
+
if (messageSimplePathElements.length > simplePathElements.length)
|
|
475
|
+
if (requestData[messageSimplePathElements[simplePathElements.length]] !== undefined) continue
|
|
461
476
|
|
|
462
477
|
nextMessages.push(message)
|
|
463
478
|
}
|
|
@@ -843,6 +858,7 @@ const handle = async function (req) {
|
|
|
843
858
|
|
|
844
859
|
// Remove draft artefacts from persistedDraft entry
|
|
845
860
|
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
861
|
+
const persistedDraftMessages = res.DraftAdministrativeData?.DraftMessages || []
|
|
846
862
|
delete res.DraftAdministrativeData_DraftUUID
|
|
847
863
|
delete res.DraftAdministrativeData
|
|
848
864
|
const HasActiveEntity = res.HasActiveEntity
|
|
@@ -866,7 +882,29 @@ const handle = async function (req) {
|
|
|
866
882
|
const upsertOptions = { headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
|
|
867
883
|
|
|
868
884
|
const _req = _newReq(req, upsertQuery, draftParams, upsertOptions)
|
|
869
|
-
|
|
885
|
+
|
|
886
|
+
let result
|
|
887
|
+
try {
|
|
888
|
+
result = await h.call(this, _req)
|
|
889
|
+
} catch (error) {
|
|
890
|
+
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && _req.errors)
|
|
891
|
+
_req.on('failed', async () => {
|
|
892
|
+
const nextDraftMessages = _compileUpdatedDraftMessages(
|
|
893
|
+
_req.errors.map(e => ({ ...e })),
|
|
894
|
+
persistedDraftMessages,
|
|
895
|
+
{},
|
|
896
|
+
draftRef
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
await cds.tx(async () => {
|
|
900
|
+
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
901
|
+
.set({ DraftMessages: nextDraftMessages })
|
|
902
|
+
.where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
throw error
|
|
907
|
+
}
|
|
870
908
|
|
|
871
909
|
// REVISIT: Remove feature flag dependency
|
|
872
910
|
if (cds.env.fiori.move_media_data_in_db) {
|
|
@@ -1203,7 +1241,9 @@ const Read = {
|
|
|
1203
1241
|
_fillIsActiveEntity(row, false, query._drafts._target)
|
|
1204
1242
|
|
|
1205
1243
|
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && row.DraftMessages?.length) {
|
|
1206
|
-
|
|
1244
|
+
// Reduce persisted draft messages to the set of those that are part of the queried entities tree
|
|
1245
|
+
|
|
1246
|
+
const simplePath = query._drafts.SELECT.from.ref
|
|
1207
1247
|
.map(refElement => {
|
|
1208
1248
|
refElement = (refElement.id ?? refElement).split('.')
|
|
1209
1249
|
if (refElement.length === 1) return refElement[0]
|
|
@@ -1213,7 +1253,8 @@ const Read = {
|
|
|
1213
1253
|
.join('/')
|
|
1214
1254
|
|
|
1215
1255
|
row.DraftMessages = row.DraftMessages.reduce((acc, msg) => {
|
|
1216
|
-
|
|
1256
|
+
const messageSimplePath = msg.prefix.replaceAll(/\([^(]*\)/g, '')
|
|
1257
|
+
if (!messageSimplePath.startsWith(simplePath)) return acc
|
|
1217
1258
|
msg.target = `/${msg.prefix}/${msg.target}`
|
|
1218
1259
|
delete msg.prefix
|
|
1219
1260
|
delete msg.simplePathElements
|
|
@@ -1221,7 +1262,6 @@ const Read = {
|
|
|
1221
1262
|
return acc
|
|
1222
1263
|
}, [])
|
|
1223
1264
|
|
|
1224
|
-
// TODO: Messsages use i18n to map codes to textual error messages -> Replace!
|
|
1225
1265
|
row.DraftMessages = getLocalizedMessages(row.DraftMessages, cds.context.http.req)
|
|
1226
1266
|
}
|
|
1227
1267
|
})
|
|
@@ -29,9 +29,10 @@ class EndpointRegistry {
|
|
|
29
29
|
if (cds.context.user._is_anonymous) return res.status(401).end()
|
|
30
30
|
next()
|
|
31
31
|
})
|
|
32
|
-
} else if (process.env.NODE_ENV === 'production') {
|
|
33
|
-
LOG.warn('Messaging endpoints not secured')
|
|
34
32
|
} else {
|
|
33
|
+
if (process.env.NODE_ENV === 'production') {
|
|
34
|
+
LOG.warn('Messaging endpoints not secured')
|
|
35
|
+
}
|
|
35
36
|
// auth middlewares set cds.context.user
|
|
36
37
|
cds.app.use(basePath, cds.middlewares.context())
|
|
37
38
|
}
|
|
@@ -28,10 +28,11 @@ class FileBasedMessaging extends MessagingService {
|
|
|
28
28
|
this.LOG._debug && this.LOG.debug('Emit', { topic: e, file: this.file })
|
|
29
29
|
try {
|
|
30
30
|
await fs.appendFile(this.file, `\n${e} ${JSON.stringify(_msg)}`)
|
|
31
|
+
unlock(this.file)
|
|
31
32
|
} catch (e) {
|
|
32
33
|
this.LOG._debug && this.LOG.debug('Error', e)
|
|
33
|
-
} finally {
|
|
34
34
|
unlock(this.file)
|
|
35
|
+
throw e
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -48,7 +48,7 @@ module.exports = class MessagingService extends cds.Service {
|
|
|
48
48
|
srv.on(event, async msg => {
|
|
49
49
|
const { data, headers } = msg
|
|
50
50
|
const messaging = await cds.connect.to('messaging') // needed for potential outbox
|
|
51
|
-
return messaging.
|
|
51
|
+
return messaging.emit({ event: topic, data, headers })
|
|
52
52
|
})
|
|
53
53
|
}
|
|
54
54
|
})
|
|
@@ -231,7 +231,7 @@ const run = async (requestConfig, options) => {
|
|
|
231
231
|
e.message = msg ? 'Error during request to remote service: ' + msg : 'Request to remote service failed.'
|
|
232
232
|
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
233
233
|
const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
|
|
234
|
-
|
|
234
|
+
cds.repl || LOG.warn(err)
|
|
235
235
|
throw err
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -50,21 +50,11 @@ function isBase64String(string, strict = false) {
|
|
|
50
50
|
return true
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
// REVISIT: when is ele._type not set and sufficient?
|
|
55
|
-
if (ele._type?.match(/^cds\./)) return ele._type
|
|
56
|
-
if (ele.type) {
|
|
57
|
-
if (ele.type.match(/^cds\./)) return ele.type
|
|
58
|
-
return resolveCDSType(ele.__proto__)
|
|
59
|
-
}
|
|
60
|
-
if (ele.items) return resolveCDSType(ele.items)
|
|
61
|
-
return ele
|
|
62
|
-
}
|
|
53
|
+
|
|
63
54
|
|
|
64
55
|
|
|
65
56
|
module.exports = {
|
|
66
57
|
getNormalizedDecimal,
|
|
67
58
|
getTarget,
|
|
68
|
-
isBase64String
|
|
69
|
-
resolveCDSType
|
|
59
|
+
isBase64String
|
|
70
60
|
}
|
|
@@ -64,14 +64,9 @@ exports.collectStreamMetadata = (result, operation, query) => {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
exports.getReadable = function readable4 (result) {
|
|
67
|
-
if (result == null) return
|
|
68
|
-
if (typeof result !== 'object') {
|
|
69
|
-
const stream = new Readable()
|
|
70
|
-
stream.push(result)
|
|
71
|
-
stream.push(null)
|
|
72
|
-
return stream
|
|
73
|
-
}
|
|
67
|
+
if (result == null) return null
|
|
74
68
|
if (result instanceof Readable) return result
|
|
75
|
-
if (result
|
|
76
|
-
if (
|
|
69
|
+
if (Array.isArray(result)) return readable4 (result[0]) // compat
|
|
70
|
+
if (typeof result === 'object' && 'value' in result) return readable4 (result.value)
|
|
71
|
+
cds.error(500, `Unexpected result type for streaming: Expected stream.Readable or null but got ${typeof result}`)
|
|
77
72
|
}
|
package/libx/http/location.js
CHANGED
package/libx/odata/index.js
CHANGED
|
@@ -259,7 +259,12 @@ const _tx_done = async (tx, responses, isJson) => {
|
|
|
259
259
|
// here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
|
|
260
260
|
rejected = 'rejected'
|
|
261
261
|
// construct commit error (without modifying original error)
|
|
262
|
-
const error = normalizeError(Object.create(e), {
|
|
262
|
+
const error = normalizeError(Object.create(e), {
|
|
263
|
+
get locale() {
|
|
264
|
+
return cds.context.locale
|
|
265
|
+
},
|
|
266
|
+
get() {}
|
|
267
|
+
})
|
|
263
268
|
// replace all responses with commit error
|
|
264
269
|
for (const res of responses) {
|
|
265
270
|
res.status = 'fail'
|
|
@@ -72,7 +72,7 @@ module.exports = (adapter, isUpsert) => {
|
|
|
72
72
|
|
|
73
73
|
handleSapMessages(cdsReq, req, res)
|
|
74
74
|
|
|
75
|
-
if (!target._isSingleton) {
|
|
75
|
+
if (!target._isSingleton && !res.hasHeader('location')) {
|
|
76
76
|
res.set('location', location4(cdsReq.target, service, result || cdsReq.data))
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -4,17 +4,8 @@ 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
|
+
else req._is_odata = true
|
|
7
8
|
if (err.details) err = _fioritized(err)
|
|
8
|
-
const content_id = req.headers['content-id']
|
|
9
|
-
if (content_id) err['@Core.ContentID'] = content_id
|
|
10
|
-
err['@Common.numericSeverity'] ??= err.numericSeverity || 4
|
|
11
|
-
|
|
12
|
-
// propagate content-id and set @Common.numericSeverity
|
|
13
|
-
err.details?.forEach(e => {
|
|
14
|
-
if (content_id) e['@Core.ContentID'] = content_id
|
|
15
|
-
e['@Common.numericSeverity'] ??= e.numericSeverity || 4 // Fiori expects this in error.details
|
|
16
|
-
})
|
|
17
|
-
|
|
18
9
|
exports.normalizeError(err, req)
|
|
19
10
|
return next(err)
|
|
20
11
|
}
|
|
@@ -25,21 +16,18 @@ exports.pass_through = err => {
|
|
|
25
16
|
}
|
|
26
17
|
|
|
27
18
|
exports.normalizeError = (err, req, cleanse = ODATA_PROPERTIES) => {
|
|
28
|
-
|
|
29
|
-
err.status = _normalize(err, locale, cleanse) || 500
|
|
19
|
+
err.status = _normalize(err, req, cleanse) || 500
|
|
30
20
|
return err
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
// TODO: This should go somewhere else ...
|
|
34
23
|
exports.getLocalizedMessages = (messages, req) => {
|
|
35
24
|
const locale = cds.i18n.locale.from(req)
|
|
36
|
-
for (let m of messages) _normalize(m,
|
|
25
|
+
for (let m of messages) _normalize(m, req, SAP_MSG_PROPERTIES, locale)
|
|
37
26
|
return messages
|
|
38
27
|
}
|
|
39
28
|
|
|
40
29
|
exports.getSapMessages = (messages, req) => {
|
|
41
|
-
|
|
42
|
-
for (let m of messages) _normalize(m, locale, SAP_MSG_PROPERTIES)
|
|
30
|
+
messages = exports.getLocalizedMessages(messages, req)
|
|
43
31
|
return JSON.stringify(messages).replace(
|
|
44
32
|
/[\u007F-\uFFFF]/g,
|
|
45
33
|
c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')
|
|
@@ -52,10 +40,13 @@ const SAP_MSG_PROPERTIES = { ...ODATA_PROPERTIES, longtextUrl: 2, transition: 2,
|
|
|
52
40
|
const BAD_REQUESTS = { ENTITY_ALREADY_EXISTS: 1, FK_CONSTRAINT_VIOLATION: 2, UNIQUE_CONSTRAINT_VIOLATION: 3 }
|
|
53
41
|
|
|
54
42
|
// prettier-ignore
|
|
55
|
-
const _normalize = (err,
|
|
43
|
+
const _normalize = (err, req, keep,
|
|
44
|
+
locale = cds.i18n.locale.from(req) || cds.env.i18n.default_language,
|
|
45
|
+
content_id = req.get('content-id')
|
|
46
|
+
) => {
|
|
56
47
|
|
|
57
48
|
// Determine status code if not already set
|
|
58
|
-
const details = err.details?.map?.(each => _normalize (each, locale,
|
|
49
|
+
const details = err.details?.map?.(each => _normalize (each, req, keep, locale, content_id))
|
|
59
50
|
const status = err.status || err.statusCode || _status4(err) || _reduce(details)
|
|
60
51
|
|
|
61
52
|
// Determine error code and message
|
|
@@ -71,7 +62,11 @@ const _normalize = (err, locale = cds.env.i18n.default_language, keep) => {
|
|
|
71
62
|
Object.defineProperty(err, 'toJSON', { value: function() {
|
|
72
63
|
const that = keep ? {} : {...this}
|
|
73
64
|
if (keep) for (let k in this) if (k in keep || k[0] === '@') that[k] = this[k]
|
|
74
|
-
if (
|
|
65
|
+
if (req._is_odata && keep !== SAP_MSG_PROPERTIES) {
|
|
66
|
+
that['@Common.numericSeverity'] ??= err.numericSeverity || 4
|
|
67
|
+
if (content_id) that['@Core.ContentID'] = content_id
|
|
68
|
+
}
|
|
69
|
+
if (locale) that.message = _message4 (err, key, locale)
|
|
75
70
|
if (!that.message) that.message = this.message
|
|
76
71
|
return that
|
|
77
72
|
}})
|
|
@@ -83,6 +78,14 @@ const _status4 = err => {
|
|
|
83
78
|
if (err.message in BAD_REQUESTS) return 400 // REVISIT: should we use 409 or 500 instead?
|
|
84
79
|
}
|
|
85
80
|
|
|
81
|
+
const _message4 = (err, key, locale) => {
|
|
82
|
+
if (err.i18n) {
|
|
83
|
+
key = /{i18n>(.*)}/.exec(err.i18n)?.[1]
|
|
84
|
+
if (!key) return err.i18n
|
|
85
|
+
}
|
|
86
|
+
return i18n.messages.at(key, locale, err.args)
|
|
87
|
+
}
|
|
88
|
+
|
|
86
89
|
const _reduce = details => {
|
|
87
90
|
const unique = [...new Set(details)]
|
|
88
91
|
if (unique.length === 1) return unique[0] // if only one unique status exists, we use that
|
|
@@ -149,7 +149,7 @@ module.exports = adapter => {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
const stream = getReadable(result)
|
|
152
|
-
if (
|
|
152
|
+
if (stream == null) return res.sendStatus(204)
|
|
153
153
|
|
|
154
154
|
validateMimetypeIsAcceptedOrThrow(req.headers, mimetype)
|
|
155
155
|
if (!res.get('content-type')) res.set('content-type', mimetype)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const cds = require('
|
|
1
|
+
const cds = require('../../..')
|
|
2
2
|
|
|
3
3
|
const { formatVal } = require('../utils')
|
|
4
4
|
|
|
@@ -65,10 +65,7 @@ function hasValidProps(obj, ...names) {
|
|
|
65
65
|
for (const propName of names) {
|
|
66
66
|
const validate = validators[propName]
|
|
67
67
|
const isValid = validate && validate(obj[propName])
|
|
68
|
-
|
|
69
|
-
if (!isValid) {
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
68
|
+
if (!isValid) return false
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
return true
|
|
@@ -183,23 +180,32 @@ function _xpr(expr, target, kind, isLambda, navPrefix = []) {
|
|
|
183
180
|
if (inExpr) res.push(`(${inExpr})`)
|
|
184
181
|
i += 1
|
|
185
182
|
} else if (_isLambda(cur, expr[i + 1])) {
|
|
183
|
+
// Process 'exists' expression
|
|
184
|
+
|
|
186
185
|
const { where } = expr[i + 1].ref.at(-1)
|
|
187
|
-
const
|
|
186
|
+
const navSegments = expr[i + 1].ref.map(ref => ref?.id ?? ref)
|
|
187
|
+
const nav = navSegments.join('/')
|
|
188
188
|
|
|
189
189
|
if (kind === 'odata-v2') {
|
|
190
190
|
// odata-v2 does not support lambda expressions but successfactors allows filter like for to-one assocs
|
|
191
191
|
cds.log('remote').info(`OData V2 does not support lambda expressions. Using path expression as best effort.`)
|
|
192
192
|
isLambda = false
|
|
193
193
|
res.push(_xpr(where, target, kind, isLambda, [...navPrefix, nav]))
|
|
194
|
-
} else if (
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
} else if (res.at(-1) === 'not' && where.length === 2 && where[0] === 'not' && where[1].xpr) {
|
|
195
|
+
// Convert double negation 'not exists where not' to 'all'
|
|
196
|
+
// > reverting the mirrored transformation performed in grammar.peggy/lambda
|
|
197
|
+
|
|
198
|
+
res.pop()
|
|
199
|
+
res.push(`${nav}/all(${LAMBDA_VARIABLE}:${_xpr(where[1].xpr, target, kind, true, navPrefix)})`)
|
|
200
|
+
} else if (where) {
|
|
197
201
|
res.push(
|
|
198
202
|
`${nav}/any(${LAMBDA_VARIABLE}:${_xpr(where, target?.elements[nav]._target, kind, true, navPrefix)})`
|
|
199
203
|
)
|
|
204
|
+
} else {
|
|
205
|
+
res.push(`${nav}/any()`)
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
i
|
|
208
|
+
i += 1
|
|
203
209
|
} else {
|
|
204
210
|
res.push(OPERATORS[cur] || cur.toLowerCase())
|
|
205
211
|
}
|
|
@@ -190,7 +190,9 @@
|
|
|
190
190
|
if (newCqn.SELECT?.recurse && cqn.where) {
|
|
191
191
|
const where = cqn.where
|
|
192
192
|
delete cqn.where
|
|
193
|
-
|
|
193
|
+
const columns = newCqn.SELECT.columns
|
|
194
|
+
delete newCqn.SELECT.columns
|
|
195
|
+
newCqn = { SELECT: { from: newCqn, where, columns } }
|
|
194
196
|
}
|
|
195
197
|
return newCqn
|
|
196
198
|
}
|
|
@@ -206,7 +208,7 @@
|
|
|
206
208
|
if (
|
|
207
209
|
(topCqn.columns && topCqn.columns[0].as === '$count') ||
|
|
208
210
|
//In QUERY_WITH_AGGREGATION topCqn.where is a having and thus no further nesting needed
|
|
209
|
-
(!QUERY_WITH_AGGREGATION && topCqn.where && (cqn.SELECT.where ||
|
|
211
|
+
(!QUERY_WITH_AGGREGATION && topCqn.where && (cqn.SELECT.where || cqn.SELECT.limit)) ||
|
|
210
212
|
(cqn.SELECT.limit && topCqn.limit) ||
|
|
211
213
|
(cqn.SELECT.orderBy && topCqn.orderBy) ||
|
|
212
214
|
(cqn.SELECT.search && topCqn.search)
|
|
@@ -710,6 +712,8 @@
|
|
|
710
712
|
= (
|
|
711
713
|
c:('*' / ref) {
|
|
712
714
|
const col = c === '*' ? {} : c
|
|
715
|
+
if (col.ref?.length > 1) throw Object.assign(new Error(`navigation "${col.ref.join('/')}" in "$expand" is not supported`), { statusCode: 400 })
|
|
716
|
+
|
|
713
717
|
col.expand = ['*']
|
|
714
718
|
if (!Array.isArray(SELECT.expand)) SELECT.expand = []
|
|
715
719
|
if (!SELECT.expand.find(_compareRefs(col))) SELECT.expand.push(col)
|
|
@@ -984,7 +988,7 @@
|
|
|
984
988
|
// Loop through each element, add it to current level, if element is already part of result, increase level
|
|
985
989
|
for(let trafos of additionalTransformation) {
|
|
986
990
|
for(const transformation in trafos) {
|
|
987
|
-
if (transformation === 'where' && (mainTransformation.groupBy ||
|
|
991
|
+
if (transformation === 'where' && (mainTransformation.groupBy || mainTransformation.aggregate) && !mainTransformation.having) {
|
|
988
992
|
//When a group by or aggregate preceed a where, the where is a having
|
|
989
993
|
mainTransformation.having = trafos[transformation]
|
|
990
994
|
}
|
|
@@ -1216,7 +1220,7 @@
|
|
|
1216
1220
|
|
|
1217
1221
|
doubleQuotedString "a doubled quoted string"
|
|
1218
1222
|
= '"' s:$('\\"' / '\\\\' / [^"])* '"'
|
|
1219
|
-
{return s.replace(
|
|
1223
|
+
{return s.replace(/\\(["\\])/g, '$1')}
|
|
1220
1224
|
|
|
1221
1225
|
word "a string"
|
|
1222
1226
|
= $([^ \t\n()"&;]+)
|