@sap/cds 9.2.1 → 9.3.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 +77 -1
- package/_i18n/i18n_es.properties +3 -3
- package/_i18n/i18n_es_MX.properties +3 -3
- package/_i18n/i18n_fr.properties +2 -2
- package/_i18n/messages.properties +6 -0
- package/app/index.js +0 -1
- package/bin/deploy.js +1 -1
- package/bin/serve.js +7 -20
- package/lib/compile/cdsc.js +3 -0
- package/lib/compile/for/flows.js +102 -0
- package/lib/compile/for/nodejs.js +28 -0
- package/lib/compile/to/edm.js +11 -4
- package/lib/core/classes.js +1 -1
- package/lib/core/linked-csn.js +8 -0
- package/lib/dbs/cds-deploy.js +12 -12
- package/lib/env/cds-env.js +1 -1
- package/lib/env/cds-requires.js +21 -20
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +5 -6
- package/lib/log/cds-log.js +6 -5
- package/lib/log/format/aspects/cf.js +2 -2
- package/lib/plugins.js +1 -1
- package/lib/ql/cds-ql.js +0 -3
- package/lib/req/request.js +3 -3
- package/lib/req/response.js +12 -7
- package/lib/srv/bindings.js +17 -17
- package/lib/srv/cds-connect.js +6 -9
- package/lib/srv/cds-serve.js +74 -137
- package/lib/srv/cds.Service.js +49 -0
- package/lib/srv/factory.js +4 -4
- package/lib/srv/middlewares/auth/ias-auth.js +29 -9
- package/lib/srv/middlewares/auth/index.js +3 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +19 -6
- package/lib/srv/protocols/hcql.js +16 -1
- package/lib/srv/srv-dispatch.js +1 -1
- package/lib/utils/cds-utils.js +4 -8
- package/lib/utils/csv-reader.js +27 -7
- package/libx/_runtime/cds.js +0 -6
- package/libx/_runtime/common/Service.js +5 -0
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/flows.js +106 -0
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/utils/differ.js +5 -15
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/fiori/lean-draft.js +76 -40
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/remote/Service.js +68 -62
- package/libx/_runtime/remote/utils/client.js +29 -216
- package/libx/_runtime/remote/utils/query.js +197 -0
- package/libx/_runtime/ucl/Service.js +180 -112
- package/libx/_runtime/ucl/queries.js +61 -0
- package/libx/odata/ODataAdapter.js +1 -4
- package/libx/odata/index.js +2 -10
- package/libx/odata/middleware/error.js +8 -1
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/middleware/update.js +12 -2
- package/libx/odata/parse/afterburner.js +113 -20
- package/libx/odata/parse/cqn2odata.js +1 -3
- package/libx/rest/middleware/parse.js +9 -2
- package/package.json +2 -2
- package/server.js +2 -0
- package/srv/app-service.js +1 -0
- package/srv/db-service.js +1 -0
- package/srv/msg-service.js +1 -0
- package/srv/remote-service.js +1 -0
- package/srv/ucl-service.cds +32 -0
- package/srv/ucl-service.js +1 -0
- package/lib/ql/resolve.js +0 -45
- package/libx/common/assert/type-strict.js +0 -109
- package/libx/common/assert/utils.js +0 -60
|
@@ -383,10 +383,15 @@ const _replaceStreams = result => {
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestData, draftsRef) => {
|
|
386
|
-
const
|
|
386
|
+
const txModel = cds.context.tx.model
|
|
387
|
+
let targetEntity = txModel.definitions[draftsRef[0].id || draftsRef[0]]
|
|
388
|
+
|
|
389
|
+
// The 'target' of validation errors needs to specify the full path from the draft root
|
|
390
|
+
// > Since success of establishment of containment can't be guaranteed, we clean up messages
|
|
391
|
+
if (!targetEntity.actions.draftActivate) return []
|
|
387
392
|
|
|
388
393
|
// Determine the path prefix required for a fully qualified validation message 'target'
|
|
389
|
-
|
|
394
|
+
const prefixRef = []
|
|
390
395
|
for (let refIdx = 0; refIdx < draftsRef.length; refIdx++) {
|
|
391
396
|
const dRef = draftsRef[refIdx],
|
|
392
397
|
dRefId = dRef.id ?? dRef
|
|
@@ -398,8 +403,7 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
|
|
|
398
403
|
const pRef = { where: [] }
|
|
399
404
|
|
|
400
405
|
if (dRefId === targetEntity.name) {
|
|
401
|
-
|
|
402
|
-
else pRef.id = targetEntity.actives.name.replace(`${targetEntity.actives._service.name}.`, '')
|
|
406
|
+
pRef.id = targetEntity.isDraft ? targetEntity.actives.name : targetEntity.name
|
|
403
407
|
} else pRef.id = dRefId
|
|
404
408
|
|
|
405
409
|
if (targetEntity.isDraft) targetEntity = targetEntity.actives
|
|
@@ -413,6 +417,7 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
|
|
|
413
417
|
if (v === undefined)
|
|
414
418
|
if (key.name === 'IsActiveEntity') v = false
|
|
415
419
|
else return null
|
|
420
|
+
if (v instanceof Buffer) v = v.toString('base64') //> convert binary keys to base64
|
|
416
421
|
if (pRef.where.length > 0) pRef.where.push('and')
|
|
417
422
|
pRef.where.push(key.name, '=', v)
|
|
418
423
|
}
|
|
@@ -427,16 +432,16 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
|
|
|
427
432
|
const nextMessages = []
|
|
428
433
|
|
|
429
434
|
// Collect messages that were created during the most recent validation run
|
|
430
|
-
const
|
|
431
|
-
message.numericSeverity ??= 4
|
|
435
|
+
const newMessagesByMessageKeyAndTarget = newMessages.reduce((acc, message) => {
|
|
436
|
+
message.numericSeverity ??= message['@Common.numericSeverity'] ?? 4
|
|
437
|
+
if (message['@Common.additionalTargets']) message.additionalTargets ??= message['@Common.additionalTargets']
|
|
438
|
+
message.additionalTargets = message.additionalTargets?.map(t => (t.startsWith('in/') ? t.slice(3) : t))
|
|
439
|
+
delete message['@Common.additionalTargets']
|
|
432
440
|
|
|
433
441
|
// Handle validation messages produced during draftActivate, that went through error normalization already
|
|
434
442
|
// > We must not store pre-localized data in DraftAdministrativeData.DraftMessages
|
|
435
443
|
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
|
-
}
|
|
444
|
+
if (message.code) delete message.code // > Expect _only_ message to be set and contain the code
|
|
440
445
|
|
|
441
446
|
// Process the message target produced by validation
|
|
442
447
|
// > The message target contains the relative path of the erroneous entity
|
|
@@ -451,23 +456,36 @@ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestDat
|
|
|
451
456
|
if (tRef.where.length > 0) tRef.where.push('and', 'IsActiveEntity', '=', false)
|
|
452
457
|
}
|
|
453
458
|
|
|
454
|
-
message.prefix = cds.odata.urlify(
|
|
459
|
+
message.prefix = cds.odata.urlify(
|
|
460
|
+
{ SELECT: { from: { ref: messagePrefixRef } } },
|
|
461
|
+
{ kind: 'odata-v4', model: txModel }
|
|
462
|
+
).path
|
|
455
463
|
|
|
456
464
|
nextMessages.push(message)
|
|
457
465
|
|
|
458
466
|
return acc.set(`${message.message}:${message.prefix}:${message.target}`, message)
|
|
459
467
|
}, new Map())
|
|
460
468
|
|
|
461
|
-
const draftMessageTargetPrefix = cds.odata.urlify(
|
|
469
|
+
const draftMessageTargetPrefix = cds.odata.urlify(
|
|
470
|
+
{ SELECT: { from: { ref: prefixRef } } },
|
|
471
|
+
{ kind: 'odata-v4', model: txModel }
|
|
472
|
+
).path
|
|
462
473
|
|
|
463
474
|
// Merge new messages with persisted ones & eliminate outdated ones
|
|
464
475
|
const simplePathElements = prefixRef.map(pRef => pRef.id)
|
|
465
476
|
for (const message of persistedMessages) {
|
|
466
477
|
// Drop persisted draft messages that are replaced by new ones
|
|
467
|
-
if (
|
|
478
|
+
if (newMessagesByMessageKeyAndTarget.has(`${message.message}:${message.prefix}:${message.target}`)) continue
|
|
479
|
+
const hasNewMessageForAdditionalTarget = message.additionalTargets?.some(target =>
|
|
480
|
+
newMessagesByMessageKeyAndTarget.has(`${message.message}:${message.prefix}:${target}`)
|
|
481
|
+
)
|
|
482
|
+
if (hasNewMessageForAdditionalTarget) continue
|
|
468
483
|
|
|
469
484
|
// Drop persisted draft messages where the value of the target field changed without a new error
|
|
470
|
-
if (message.prefix === draftMessageTargetPrefix
|
|
485
|
+
if (message.prefix === draftMessageTargetPrefix) {
|
|
486
|
+
if (requestData[message.target] !== undefined) continue
|
|
487
|
+
if (message.additionalTargets?.some(target => requestData[target] !== undefined)) continue
|
|
488
|
+
}
|
|
471
489
|
|
|
472
490
|
// Drop persisted draft messages, whose target's use navigations, where the value of the target field changed without a new error
|
|
473
491
|
const messageSimplePathElements = message.prefix.replaceAll(/\([^(]*\)/g, '').split('/')
|
|
@@ -562,9 +580,7 @@ const handle = async function (req) {
|
|
|
562
580
|
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
|
|
563
581
|
if (_req.target.isDraft && (_req.event === 'UPDATE' || _req.event === 'NEW')) {
|
|
564
582
|
// Degrade all errors to messages & prevent !!req.errors into req.reject() in dispatch
|
|
565
|
-
_req.error = (...args) =>
|
|
566
|
-
for (const err of args) _req._messages.add(4, err)
|
|
567
|
-
}
|
|
583
|
+
_req.error = (...args) => _req._messages.add(4, ...args)
|
|
568
584
|
}
|
|
569
585
|
}
|
|
570
586
|
|
|
@@ -681,7 +697,10 @@ const handle = async function (req) {
|
|
|
681
697
|
pRef.where.push('and', 'IsActiveEntity', '=', false)
|
|
682
698
|
return pRef
|
|
683
699
|
})
|
|
684
|
-
const messageTargetPrefix = cds.odata.urlify(
|
|
700
|
+
const messageTargetPrefix = cds.odata.urlify(
|
|
701
|
+
{ SELECT: { from: { ref: prefixRef } } },
|
|
702
|
+
{ kind: 'odata-v4', model: req.context.tx.model }
|
|
703
|
+
).path
|
|
685
704
|
|
|
686
705
|
const draftData = await SELECT.one
|
|
687
706
|
.from({ ref: _redirectRefToDrafts(query.DELETE.from.ref, this.model) }) // REVISIT: Avoid redundant redirect
|
|
@@ -694,7 +713,8 @@ const handle = async function (req) {
|
|
|
694
713
|
const persistedDraftMessages = draftData?.DraftAdministrativeData?.DraftMessages || []
|
|
695
714
|
|
|
696
715
|
if (draftAdminDataUUID && persistedDraftMessages?.length) {
|
|
697
|
-
|
|
716
|
+
let nextDraftMessages = persistedDraftMessages.filter(msg => !msg.prefix.startsWith(messageTargetPrefix))
|
|
717
|
+
if (!req.target.actions?.draftActivate) nextDraftMessages = []
|
|
698
718
|
|
|
699
719
|
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
700
720
|
.set({ DraftMessages: nextDraftMessages })
|
|
@@ -890,7 +910,10 @@ const handle = async function (req) {
|
|
|
890
910
|
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && _req.errors)
|
|
891
911
|
_req.on('failed', async () => {
|
|
892
912
|
const nextDraftMessages = _compileUpdatedDraftMessages(
|
|
893
|
-
|
|
913
|
+
// Errors procesed during 'failed' will have undergone error._normalize at this point
|
|
914
|
+
// > We need to revert the code - message swap _normalize includes
|
|
915
|
+
// > This is required to ensure, no localized messages are persisted and redundant localization is avoided
|
|
916
|
+
_req.errors.map(e => ({ message: e.code ?? e.message, target: e.target, args: e.args, i18n: e.i18n })),
|
|
894
917
|
persistedDraftMessages,
|
|
895
918
|
{},
|
|
896
919
|
draftRef
|
|
@@ -999,7 +1022,16 @@ const handle = async function (req) {
|
|
|
999
1022
|
})
|
|
1000
1023
|
}
|
|
1001
1024
|
|
|
1002
|
-
|
|
1025
|
+
const innerQuery = UPDATE({ ref: draftsRef }).data(req.data)
|
|
1026
|
+
const innerReq = _newReq(req, innerQuery, draftParams, {})
|
|
1027
|
+
await h.call(this, innerReq).catch(error => {
|
|
1028
|
+
// > This prevents thrown req.errors from being added to 'sap-messages'
|
|
1029
|
+
// > Downgraded req.error will add to req.messages, regardless of whether the error is thrown
|
|
1030
|
+
// > Unless we prevent it here, the response will contain the error in both body and header
|
|
1031
|
+
// > Downgrading is only required for PATCH, to prevent a rollback in case validation fails
|
|
1032
|
+
if (req.messages?.length) req.messages = req.messages.filter(m => m !== error)
|
|
1033
|
+
throw req.error(error)
|
|
1034
|
+
})
|
|
1003
1035
|
|
|
1004
1036
|
const nextDraftAdminData = {
|
|
1005
1037
|
InProcessByUser: req.user.id,
|
|
@@ -1156,11 +1188,7 @@ const Read = {
|
|
|
1156
1188
|
let drafts
|
|
1157
1189
|
if (ignoreDrafts) drafts = []
|
|
1158
1190
|
else {
|
|
1159
|
-
|
|
1160
|
-
drafts = await Read.complementaryDrafts(query, actives)
|
|
1161
|
-
} catch {
|
|
1162
|
-
drafts = []
|
|
1163
|
-
}
|
|
1191
|
+
drafts = await Read.complementaryDrafts(query, actives)
|
|
1164
1192
|
}
|
|
1165
1193
|
Read.merge(query._target, actives, drafts, (row, other) => {
|
|
1166
1194
|
if (other) {
|
|
@@ -1218,7 +1246,12 @@ const Read = {
|
|
|
1218
1246
|
return result
|
|
1219
1247
|
}
|
|
1220
1248
|
|
|
1221
|
-
|
|
1249
|
+
const selectedColumnNames = query._drafts.SELECT.columns?.map(c => c.ref?.[0] || c) || ['*']
|
|
1250
|
+
const isAllKeysSelected =
|
|
1251
|
+
selectedColumnNames.includes('*') ||
|
|
1252
|
+
Object.keys(query._drafts._target.keys).every(k => selectedColumnNames.includes(k))
|
|
1253
|
+
|
|
1254
|
+
if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && isAllKeysSelected) {
|
|
1222
1255
|
// Replace selection of 'DraftMessages' with the proper path expression
|
|
1223
1256
|
|
|
1224
1257
|
query._drafts.SELECT.columns = [
|
|
@@ -1256,6 +1289,8 @@ const Read = {
|
|
|
1256
1289
|
const messageSimplePath = msg.prefix.replaceAll(/\([^(]*\)/g, '')
|
|
1257
1290
|
if (!messageSimplePath.startsWith(simplePath)) return acc
|
|
1258
1291
|
msg.target = `/${msg.prefix}/${msg.target}`
|
|
1292
|
+
if (msg.additionalTargets?.length)
|
|
1293
|
+
msg.additionalTargets = msg.additionalTargets.map(additionalTarget => `/${msg.prefix}/${additionalTarget}`)
|
|
1259
1294
|
delete msg.prefix
|
|
1260
1295
|
delete msg.simplePathElements
|
|
1261
1296
|
acc.push(msg)
|
|
@@ -1502,21 +1537,22 @@ const Read = {
|
|
|
1502
1537
|
complementaryDrafts: (query, _actives) => {
|
|
1503
1538
|
const actives = Array.isArray(_actives) ? _actives : [_actives]
|
|
1504
1539
|
if (!actives.length) return []
|
|
1505
|
-
const drafts =
|
|
1540
|
+
const drafts = SELECT.from(query._target.drafts)
|
|
1541
|
+
drafts[$draftParams] = query[$draftParams]
|
|
1542
|
+
if (query._srv) drafts._srv = query._srv
|
|
1506
1543
|
drafts.SELECT.where = Read.whereIn(query._target, actives)
|
|
1507
1544
|
const newColumns = _entityKeys(query._target).map(k => ({ ref: [k] }))
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1545
|
+
const queryDrafts = query._drafts
|
|
1546
|
+
if (queryDrafts) {
|
|
1547
|
+
if (
|
|
1548
|
+
!queryDrafts.SELECT.columns ||
|
|
1549
|
+
queryDrafts.SELECT.columns.some(c => c === '*' || c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')
|
|
1550
|
+
)
|
|
1551
|
+
newColumns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
|
|
1552
|
+
const draftAdmin = queryDrafts.SELECT.columns?.find(c => c.ref?.[0] === 'DraftAdministrativeData')
|
|
1553
|
+
if (draftAdmin) newColumns.push(draftAdmin)
|
|
1554
|
+
}
|
|
1515
1555
|
drafts.SELECT.columns = newColumns
|
|
1516
|
-
drafts.SELECT.count = undefined
|
|
1517
|
-
drafts.SELECT.search = undefined
|
|
1518
|
-
drafts.SELECT.one = undefined
|
|
1519
|
-
drafts.SELECT.recurse = undefined
|
|
1520
1556
|
return drafts
|
|
1521
1557
|
},
|
|
1522
1558
|
|
|
@@ -2073,7 +2109,7 @@ async function onEdit(req) {
|
|
|
2073
2109
|
res[key] = keyData[key]
|
|
2074
2110
|
return res
|
|
2075
2111
|
}, {})
|
|
2076
|
-
const transition = cds.
|
|
2112
|
+
const transition = cds.db.resolve.transitions(req.query)
|
|
2077
2113
|
|
|
2078
2114
|
// gets the underlying target entity, as record locking can't be
|
|
2079
2115
|
// applied to localized views
|
|
@@ -30,7 +30,7 @@ const _checkAppURL = appURL => {
|
|
|
30
30
|
// REVISIT: It's bad to have to rely on the subdomain.
|
|
31
31
|
// For all interactions where we perform the token exchange ourselves,
|
|
32
32
|
// we will be able to use the zoneId instead of the subdomain.
|
|
33
|
-
const _subdomainFromContext = context => context?.
|
|
33
|
+
const _subdomainFromContext = context => context?.user?.authInfo?.getSubdomain?.()
|
|
34
34
|
|
|
35
35
|
class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
36
36
|
init() {
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
|
-
|
|
3
|
-
const { run, getReqOptions } = require('./utils/client')
|
|
4
2
|
const { getCloudSdk, getCloudSdkConnectivity, getCloudSdkResilience } = require('./utils/cloudSdkProvider')
|
|
3
|
+
const { run } = require('./utils/client')
|
|
4
|
+
const { extractRequestConfig } = require('./utils/query')
|
|
5
5
|
const { hasAliasedColumns } = require('./utils/data')
|
|
6
6
|
const postProcess = require('../common/utils/postProcess')
|
|
7
7
|
const { formatVal } = require('../../odata/utils')
|
|
8
8
|
|
|
9
9
|
const _getHeaders = (defaultHeaders, req) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}, {})
|
|
17
|
-
)
|
|
10
|
+
const normalizedHeaders = Object.keys(req.headers).reduce((acc, cur) => {
|
|
11
|
+
acc[cur.toLowerCase()] = req.headers[cur]
|
|
12
|
+
return acc
|
|
13
|
+
}, {})
|
|
14
|
+
|
|
15
|
+
return { ...defaultHeaders, ...normalizedHeaders }
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
const _setCorrectValue = (el, data, params, kind) => {
|
|
@@ -159,25 +157,23 @@ const _isSelectWithAliasedColumns = q => q?.SELECT && !q._transitions && q.SELEC
|
|
|
159
157
|
|
|
160
158
|
const resolvedTargetOfQuery = q => q?._transitions?.at(-1)?.target
|
|
161
159
|
|
|
162
|
-
const _resolveSelectionStrategy =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
160
|
+
const _resolveSelectionStrategy = selectionStrategy => {
|
|
161
|
+
const { DestinationSelectionStrategies } = getCloudSdkConnectivity()
|
|
162
|
+
const strategy = DestinationSelectionStrategies[selectionStrategy]
|
|
163
|
+
if (typeof strategy !== 'function')
|
|
164
|
+
throw new Error(`Unsupported destination selection strategy "${selectionStrategy}".`)
|
|
165
|
+
return strategy
|
|
169
166
|
}
|
|
170
167
|
|
|
171
168
|
const _getKind = options => {
|
|
172
|
-
|
|
169
|
+
let kind = options.credentials?.kind ?? options.kind
|
|
173
170
|
if (typeof kind === 'object') {
|
|
174
|
-
|
|
175
|
-
key => key === 'odata' || key === 'odata-v4' || key === 'odata-v2' || key === 'rest'
|
|
171
|
+
kind = Object.keys(kind).find(
|
|
172
|
+
key => key === 'odata' || key === 'odata-v4' || key === 'odata-v2' || key === 'rest' || key === 'hcql'
|
|
176
173
|
)
|
|
177
|
-
// odata-v4 is equivalent of odata
|
|
178
|
-
return k === 'odata-v4' ? 'odata' : k
|
|
179
174
|
}
|
|
180
|
-
|
|
175
|
+
// odata-v4 is equivalent of odata
|
|
176
|
+
return kind === 'odata-v4' ? 'odata' : kind
|
|
181
177
|
}
|
|
182
178
|
|
|
183
179
|
const _getDestination = (name, credentials) => {
|
|
@@ -188,49 +184,48 @@ const _getDestination = (name, credentials) => {
|
|
|
188
184
|
|
|
189
185
|
class RemoteService extends cds.Service {
|
|
190
186
|
init() {
|
|
187
|
+
if (([...this.entities].length || [...this.operations].length) && !this.options.credentials)
|
|
188
|
+
throw new Error(`No credentials configured for "${this.name}".`)
|
|
189
|
+
|
|
191
190
|
this.kind = _getKind(this.options) // TODO: Simplify
|
|
192
191
|
|
|
193
|
-
/*
|
|
194
|
-
* set up connectivity stuff if credentials are provided
|
|
195
|
-
* throw error if no credentials are provided and the service has at least one entity or one action/function
|
|
196
|
-
*/
|
|
197
192
|
if (this.options.credentials) {
|
|
198
193
|
this.datasource = this.options.datasource
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.
|
|
202
|
-
this.
|
|
203
|
-
|
|
194
|
+
|
|
195
|
+
this.destinationOptions = this.options.destinationOptions || {}
|
|
196
|
+
if (typeof this.destinationOptions.selectionStrategy === 'string') {
|
|
197
|
+
this.destinationOptions.selectionStrategy = _resolveSelectionStrategy(this.destinationOptions.selectionStrategy)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this.options.credentials.destination) this.destination = this.options.credentials.destination
|
|
201
|
+
else this.destination = _getDestination(this.definition?.name ?? this.datasource, this.options.credentials)
|
|
202
|
+
|
|
204
203
|
this.path = this.options.credentials.path
|
|
205
204
|
|
|
206
|
-
// `requestTimeout`
|
|
205
|
+
// API `requestTimeout` is kept because it once was public
|
|
207
206
|
this.requestTimeout = this.options.credentials.requestTimeout ?? 60000
|
|
208
207
|
|
|
209
208
|
this.csrf = this.options.csrf
|
|
210
209
|
this.csrfInBatch = this.options.csrfInBatch
|
|
211
210
|
|
|
212
|
-
//
|
|
213
|
-
// required for BAS creating remote services only for events
|
|
214
|
-
// at first request the
|
|
211
|
+
// We're using this as an object to allow remote services without the need for Cloud SDK
|
|
212
|
+
// This is required for BAS creating remote services only for events
|
|
213
|
+
// Middlewares are created at time of the first request, on the fly
|
|
215
214
|
this.middlewares = {
|
|
216
215
|
timeout: getCloudSdkResilience().timeout(this.requestTimeout),
|
|
217
216
|
csrf: this.csrf && getCloudSdk().csrf(this.csrf)
|
|
218
217
|
}
|
|
219
|
-
} else if ([...this.entities].length || [...this.operations].length) {
|
|
220
|
-
throw new Error(`No credentials configured for "${this.name}".`)
|
|
221
218
|
}
|
|
222
219
|
|
|
220
|
+
// Add 'initial' handler to clear keys from data
|
|
223
221
|
const clearKeysFromData = function (req) {
|
|
224
222
|
if (req.target && req.target.keys) for (const k of Object.keys(req.target.keys)) delete req.data[k]
|
|
225
223
|
}
|
|
226
224
|
this.before('UPDATE', '*', Object.assign(clearKeysFromData, { _initial: true }))
|
|
227
225
|
|
|
228
|
-
for
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
226
|
+
// Add handlers for bound & unbound operations
|
|
227
|
+
for (const each of this.entities)
|
|
228
|
+
for (const a in each.actions) _addHandlerActionFunction(this, each.actions[a], each)
|
|
234
229
|
for (const each of this.operations) _addHandlerActionFunction(this, each)
|
|
235
230
|
|
|
236
231
|
// IMPORTANT: regular function is used on purpose, don't switch to arrow function.
|
|
@@ -238,39 +233,50 @@ class RemoteService extends cds.Service {
|
|
|
238
233
|
const { query } = req
|
|
239
234
|
if (!query && !(typeof req.path === 'string')) return next()
|
|
240
235
|
|
|
241
|
-
//
|
|
242
|
-
//
|
|
236
|
+
// Early validation on first request for use case without remote API
|
|
237
|
+
// Ideally, that's done on bootstrap of the remote service
|
|
243
238
|
if (typeof this.destination === 'object' && !this.destination.url)
|
|
244
239
|
throw new Error(`"url" or "destination" property must be configured in "credentials" of "${this.name}".`)
|
|
245
240
|
|
|
246
|
-
const
|
|
247
|
-
|
|
241
|
+
const requestConfig = extractRequestConfig(req, query, this)
|
|
242
|
+
requestConfig.headers = _getHeaders(requestConfig.headers, req)
|
|
248
243
|
|
|
249
244
|
// REVISIT: we should not have to set the content-type at all for that
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
const correlationId =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
245
|
+
if (requestConfig.headers.accept?.match(/stream|image|tar/)) requestConfig.responseType = 'stream'
|
|
246
|
+
|
|
247
|
+
// Ensure request correlation (even with systems that use x-correlationid)
|
|
248
|
+
const correlationId = requestConfig.headers['x-correlation-id'] || cds.context?.id
|
|
249
|
+
requestConfig.headers['x-correlation-id'] = correlationId
|
|
250
|
+
requestConfig.headers['x-correlationid'] = correlationId
|
|
251
|
+
|
|
252
|
+
// Compile Cloud SDK options
|
|
253
|
+
const additionalOptions = {
|
|
254
|
+
kind: this.kind,
|
|
255
|
+
destination: this.destination,
|
|
256
|
+
destinationOptions: this.destinationOptions,
|
|
257
|
+
resolvedTarget:
|
|
258
|
+
resolvedTargetOfQuery(query) ||
|
|
259
|
+
(typeof query === 'object' ? cds.infer.target(query) || query?._target : undefined) ||
|
|
260
|
+
req.target,
|
|
261
|
+
returnType: req._returnType
|
|
262
|
+
}
|
|
262
263
|
|
|
263
264
|
// REVISIT: i don't believe req.context.headers is an official API
|
|
264
265
|
let jwt = req?.context?.headers?.authorization?.split(/^bearer /i)[1]
|
|
265
266
|
if (!jwt) jwt = req?.context?.http?.req?.headers?.authorization?.split(/^bearer /i)[1]
|
|
266
267
|
if (jwt) additionalOptions.jwt = jwt
|
|
267
268
|
|
|
268
|
-
//
|
|
269
|
+
// Mount resilience and csrf middlewares for SAP Cloud SDK
|
|
270
|
+
requestConfig.middleware = [this.middlewares.timeout]
|
|
271
|
+
const fetchCsrfToken = !!(requestConfig._autoBatchedGet ? this.csrfInBatch : this.csrf)
|
|
272
|
+
if (fetchCsrfToken) requestConfig.middleware.push(this.middlewares.csrf)
|
|
273
|
+
|
|
274
|
+
// Hidden compat flag to suppress logging the response body of failed request
|
|
269
275
|
if (req._suppressRemoteResponseBody) {
|
|
270
276
|
additionalOptions.suppressRemoteResponseBody = req._suppressRemoteResponseBody
|
|
271
277
|
}
|
|
272
278
|
|
|
273
|
-
let result = await run(
|
|
279
|
+
let result = await run(requestConfig, additionalOptions)
|
|
274
280
|
result = typeof query === 'object' && query.SELECT?.one && Array.isArray(result) ? result[0] : result
|
|
275
281
|
return result
|
|
276
282
|
})
|