@sap/cds 7.8.1 → 7.9.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 +44 -0
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +3 -0
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_es_MX.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/bin/serve.js +5 -5
- package/lib/auth/basic-auth.js +1 -1
- package/lib/compile/cdsc.js +33 -6
- package/lib/compile/etc/_localized.js +14 -7
- package/lib/compile/for/lean_drafts.js +9 -0
- package/lib/compile/to/edm-files.js +116 -0
- package/lib/compile/to/edm.js +8 -1
- package/lib/compile/to/hdbtabledata.js +3 -3
- package/lib/compile/to/sql.js +4 -2
- package/lib/compile/to/yaml.js +22 -21
- package/lib/dbs/cds-deploy.js +5 -6
- package/lib/env/cds-env.js +7 -0
- package/lib/env/cds-requires.js +20 -1
- package/lib/env/defaults.js +21 -5
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +85 -4
- package/lib/index.js +1 -1
- package/lib/linked/classes.js +2 -2
- package/lib/linked/entities.js +10 -0
- package/lib/linked/models.js +1 -1
- package/lib/plugins.js +1 -1
- package/lib/ql/INSERT.js +17 -3
- package/lib/ql/Query.js +4 -0
- package/lib/ql/infer.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/srv/cds-serve.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/protocols/odata-v4.js +5 -6
- package/lib/srv/srv-models.js +9 -2
- package/lib/utils/cds-test.js +2 -0
- package/lib/utils/cds-utils.js +9 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
- package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
- package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
- package/libx/_runtime/common/generic/auth/index.js +2 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
- package/libx/_runtime/common/generic/auth/restrict.js +6 -5
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +5 -8
- package/libx/_runtime/common/generic/etag.js +8 -6
- package/libx/_runtime/common/generic/sorting.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
- package/libx/_runtime/common/utils/compareJson.js +274 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
- package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
- package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
- package/libx/_runtime/common/utils/resolveView.js +0 -16
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +9 -2
- package/libx/_runtime/common/utils/ucsn.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/generic/rewrite.js +7 -13
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +151 -46
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/execute.js +6 -2
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
- package/libx/_runtime/messaging/event-broker.js +212 -0
- package/libx/_runtime/remote/Service.js +9 -32
- package/libx/_runtime/remote/utils/client.js +13 -21
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
- package/libx/_runtime/sqlite/execute.js +8 -3
- package/libx/_runtime/ucl/Service.js +259 -0
- package/libx/common/assert/index.js +6 -11
- package/libx/common/assert/validation.js +6 -1
- package/libx/odata/index.js +47 -25
- package/libx/odata/middleware/batch.js +8 -7
- package/libx/odata/middleware/create.js +42 -16
- package/libx/odata/middleware/delete.js +18 -11
- package/libx/odata/middleware/metadata.js +15 -14
- package/libx/odata/middleware/operation.js +30 -40
- package/libx/odata/middleware/parse.js +2 -3
- package/libx/odata/middleware/read.js +59 -52
- package/libx/odata/middleware/service-document.js +7 -7
- package/libx/odata/middleware/stream.js +26 -24
- package/libx/odata/middleware/update.js +53 -92
- package/libx/odata/parse/afterburner.js +45 -47
- package/libx/odata/parse/grammar.peggy +3 -3
- package/libx/odata/parse/multipartToJson.js +10 -22
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +13 -0
- package/libx/odata/utils/handler.js +120 -0
- package/libx/odata/utils/index.js +15 -2
- package/libx/odata/utils/metaInfo.js +410 -0
- package/libx/odata/utils/path.js +5 -2
- package/libx/odata/utils/readAfterWrite.js +23 -0
- package/libx/odata/utils/result.js +4 -5
- package/libx/rest/RestAdapter.js +4 -13
- package/libx/rest/middleware/parse.js +40 -7
- package/package.json +1 -1
- package/server.js +2 -1
- package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
- package/libx/_runtime/common/utils/thenable.js +0 -51
- package/libx/_runtime/rest/service.js +0 -2
- package/libx/odata/parse/parseToCqn.js +0 -39
- package/libx/rest/middleware/input.js +0 -54
- package/libx/rest/middleware/payload.js +0 -13
|
@@ -5,6 +5,9 @@ const { getKeyData } = require('./utils/where')
|
|
|
5
5
|
|
|
6
6
|
const { getPageSize, commonGenericPaging } = require('../common/generic/paging')
|
|
7
7
|
const { handler: commonGenericSorting } = require('../common/generic/sorting')
|
|
8
|
+
const { addEtagColumns } = require('../common/utils/etag')
|
|
9
|
+
|
|
10
|
+
const { calculateLocationHeader } = require('../../odata/utils')
|
|
8
11
|
|
|
9
12
|
const LOG = cds.log('fiori|drafts')
|
|
10
13
|
const original = Symbol('original')
|
|
@@ -73,10 +76,10 @@ const LOCK_TIMEOUT = {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
const reject_bypassed_draft = req => {
|
|
76
|
-
const
|
|
79
|
+
const message =
|
|
77
80
|
!cds.profiles?.includes('production') &&
|
|
78
81
|
'`cds.env.fiori.bypass_draft` must be enabled or the entity must be annotated with `@odata.draft.bypass` to support the directly modification of active instances.'
|
|
79
|
-
return req.reject(501,
|
|
82
|
+
return req.reject({ code: 501, statusCode: 501, message })
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
const DRAFT_ELEMENTS = new Set([
|
|
@@ -322,8 +325,10 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
322
325
|
return result
|
|
323
326
|
}
|
|
324
327
|
|
|
328
|
+
if (req.event === 'draftEdit') req.event = 'EDIT'
|
|
329
|
+
|
|
325
330
|
if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
|
|
326
|
-
if (req.event === 'draftPrepare' && draftParams.IsActiveEntity) req.reject(400)
|
|
331
|
+
if (req.event === 'draftPrepare' && draftParams.IsActiveEntity) req.reject({ code: 400, statusCode: 400 })
|
|
327
332
|
if (req.event === 'NEW' && req.data?.IsActiveEntity === true) {
|
|
328
333
|
if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass']) return reject_bypassed_draft(req)
|
|
329
334
|
const containsDraftRoot =
|
|
@@ -331,7 +336,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
331
336
|
'@Common.DraftRoot.ActivationAction'
|
|
332
337
|
]
|
|
333
338
|
|
|
334
|
-
if (!containsDraftRoot) req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
|
|
339
|
+
if (!containsDraftRoot) req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
|
|
335
340
|
|
|
336
341
|
const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
337
342
|
const data = Object.assign({}, req.data) // IsActiveEntity is not enumerable
|
|
@@ -355,7 +360,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
355
360
|
rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef }).where(keyData)
|
|
356
361
|
}
|
|
357
362
|
|
|
358
|
-
if (rootHasDraft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
363
|
+
if (rootHasDraft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
359
364
|
|
|
360
365
|
const cqn = INSERT.into(query.INSERT.into).entries(data)
|
|
361
366
|
await run(cqn, { event: 'CREATE' })
|
|
@@ -393,8 +398,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
393
398
|
const draft = await draftQuery
|
|
394
399
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
395
400
|
if (inProcessByUser && inProcessByUser !== cds.context.user.id)
|
|
396
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [inProcessByUser])
|
|
397
|
-
if (draft) req.reject(403, 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS')
|
|
401
|
+
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [inProcessByUser] })
|
|
402
|
+
if (draft) req.reject({ code: 403, statusCode: 403, message: 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS' })
|
|
398
403
|
await run(query)
|
|
399
404
|
return req.data
|
|
400
405
|
}
|
|
@@ -405,11 +410,15 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
405
410
|
// It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
|
|
406
411
|
// be implemented in expand implementation.
|
|
407
412
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
|
|
408
|
-
req.reject(
|
|
413
|
+
req.reject({
|
|
414
|
+
code: 400,
|
|
415
|
+
statusCode: 400,
|
|
416
|
+
message: 'Action "draftActivate" can only be called on the root draft entity'
|
|
417
|
+
})
|
|
409
418
|
}
|
|
410
419
|
|
|
411
420
|
if (req.target._etag && !req.headers['if-match'] && !req.headers['if-none-match']) {
|
|
412
|
-
req.reject(428)
|
|
421
|
+
req.reject({ code: 428, statusCode: 428 })
|
|
413
422
|
}
|
|
414
423
|
|
|
415
424
|
const targetDraft = req.target.drafts
|
|
@@ -428,9 +437,15 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
428
437
|
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
429
438
|
])
|
|
430
439
|
)
|
|
431
|
-
if (!res)
|
|
440
|
+
if (!res)
|
|
441
|
+
req.reject(_etagValidationType ? { code: 412, statusCode: 412 } : { code: 'DRAFT_NOT_EXISTING', statusCode: 404 })
|
|
432
442
|
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
433
|
-
req.reject(
|
|
443
|
+
req.reject({
|
|
444
|
+
code: 403,
|
|
445
|
+
statusCode: 403,
|
|
446
|
+
message: 'DRAFT_LOCKED_BY_ANOTHER_USER',
|
|
447
|
+
args: [res.DraftAdministrativeData?.InProcessByUser]
|
|
448
|
+
})
|
|
434
449
|
}
|
|
435
450
|
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
436
451
|
delete res.DraftAdministrativeData_DraftUUID
|
|
@@ -469,12 +484,21 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
469
484
|
// REVISIT: needs reworking for new adapter, especially re $batch
|
|
470
485
|
if (req._?.odataRes) {
|
|
471
486
|
req._?.odataRes?.setStatusCode(201, { overwrite: true })
|
|
472
|
-
} else if (req.
|
|
473
|
-
req.
|
|
487
|
+
} else if (req.res) {
|
|
488
|
+
req.res.status(201)
|
|
474
489
|
}
|
|
475
490
|
}
|
|
476
491
|
|
|
477
|
-
|
|
492
|
+
if (cds.env.features.odata_new_adapter) {
|
|
493
|
+
const read_result = await _readAfterDraftAction.bind(this)({ req, payload: res, action: 'draftActivate' })
|
|
494
|
+
req.res.set(
|
|
495
|
+
'location',
|
|
496
|
+
'../' + calculateLocationHeader(req.target, this, read_result || { ...res, IsActiveEntity: true })
|
|
497
|
+
)
|
|
498
|
+
return read_result
|
|
499
|
+
} else {
|
|
500
|
+
return Object.assign(result, { IsActiveEntity: true })
|
|
501
|
+
}
|
|
478
502
|
}
|
|
479
503
|
|
|
480
504
|
if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
|
|
@@ -484,27 +508,35 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
484
508
|
rootQuery.SELECT.one = true
|
|
485
509
|
rootQuery.SELECT.from = { ref: [query.SELECT.from.ref[0]] }
|
|
486
510
|
const root = await cds.run(rootQuery)
|
|
487
|
-
if (!root) req.reject(404)
|
|
488
|
-
if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
511
|
+
if (!root) req.reject({ code: 404, statusCode: 404 })
|
|
512
|
+
if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
513
|
+
req.reject({ code: 403, statusCode: 403 })
|
|
514
|
+
}
|
|
489
515
|
const _req = _newReq(req, query, draftParams, { event: req.event })
|
|
490
516
|
const result = await handle(_req)
|
|
491
517
|
return result
|
|
492
518
|
}
|
|
493
519
|
|
|
494
520
|
if (req.event === 'PATCH') {
|
|
495
|
-
if (!('IsActiveEntity' in draftParams)) req.reject(501)
|
|
521
|
+
if (!('IsActiveEntity' in draftParams)) req.reject({ code: 501, statusCode: 501 })
|
|
496
522
|
|
|
497
523
|
if (draftParams.IsActiveEntity === false) {
|
|
498
524
|
LOG.debug('patch draft')
|
|
499
|
-
if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject(405)
|
|
525
|
+
if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject({ code: 405, statusCode: 405 })
|
|
500
526
|
const draftsRef = _redirectRefToDrafts(query.UPDATE.entity.ref, this.model)
|
|
501
527
|
const res = await SELECT.one.from({ ref: draftsRef }).columns('DraftAdministrativeData_DraftUUID', {
|
|
502
528
|
ref: ['DraftAdministrativeData'],
|
|
503
529
|
expand: [{ ref: ['InProcessByUser'] }]
|
|
504
530
|
})
|
|
505
|
-
if (!res) req.reject(404)
|
|
506
|
-
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
507
|
-
req.reject(
|
|
531
|
+
if (!res) req.reject({ code: 404, statusCode: 404 })
|
|
532
|
+
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
|
|
533
|
+
req.reject({
|
|
534
|
+
code: 403,
|
|
535
|
+
statusCode: 403,
|
|
536
|
+
message: 'DRAFT_LOCKED_BY_ANOTHER_USER',
|
|
537
|
+
args: [res.DraftAdministrativeData?.InProcessByUser]
|
|
538
|
+
})
|
|
539
|
+
}
|
|
508
540
|
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
509
541
|
.data({
|
|
510
542
|
InProcessByUser: req.user.id,
|
|
@@ -527,12 +559,12 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
527
559
|
const entityRef = query.UPDATE.entity.ref
|
|
528
560
|
|
|
529
561
|
if (!this.model.definitions[entityRef[0].id]['@Common.DraftRoot.ActivationAction']) {
|
|
530
|
-
req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
|
|
562
|
+
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
|
|
531
563
|
}
|
|
532
564
|
|
|
533
565
|
const draftsRef = _redirectRefToDrafts(entityRef, this.model)
|
|
534
566
|
const hasDraft = !!(await SELECT.one([1]).from({ ref: [draftsRef[0]] }))
|
|
535
|
-
if (hasDraft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
567
|
+
if (hasDraft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
536
568
|
|
|
537
569
|
await run(query)
|
|
538
570
|
return req.data
|
|
@@ -1018,7 +1050,13 @@ function _cleansed(query, model) {
|
|
|
1018
1050
|
const target = query._target
|
|
1019
1051
|
const q = cds.ql.clone(query)
|
|
1020
1052
|
|
|
1021
|
-
|
|
1053
|
+
if (q.SELECT?.from.SELECT) q.SELECT.from = _cleanseQuery(q.SELECT?.from, draftParams, model)
|
|
1054
|
+
const ref =
|
|
1055
|
+
q.SELECT?.from.SELECT?.from.ref ||
|
|
1056
|
+
q.SELECT?.from.ref ||
|
|
1057
|
+
q.UPDATE?.entity.ref ||
|
|
1058
|
+
q.INSERT?.into.ref ||
|
|
1059
|
+
q.DELETE?.from.ref
|
|
1022
1060
|
const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE
|
|
1023
1061
|
|
|
1024
1062
|
if (ref) {
|
|
@@ -1028,7 +1066,7 @@ function _cleansed(query, model) {
|
|
|
1028
1066
|
if (!entity?.drafts) return r
|
|
1029
1067
|
return r.where ? { ...r, where: _cleanseWhere(r.where, draftParams, DRAFT_ELEMENTS) } : r
|
|
1030
1068
|
})
|
|
1031
|
-
if (q.SELECT) q.SELECT.from = { ...q.SELECT.from, ref: cleansedRef }
|
|
1069
|
+
if (q.SELECT) q.SELECT.from = q.SELECT.from.SELECT ? q.SELECT.from : { ...q.SELECT.from, ref: cleansedRef }
|
|
1032
1070
|
else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
|
|
1033
1071
|
else if (q.UPDATE) q.UPDATE.entity = { ...q.UPDATE.entity, ref: cleansedRef }
|
|
1034
1072
|
else if (q.INSERT) q.INSERT.into = { ...q.INSERT.into, ref: cleansedRef }
|
|
@@ -1175,11 +1213,11 @@ function expandStarStar(target, recursion = new Map()) {
|
|
|
1175
1213
|
async function onNew(req) {
|
|
1176
1214
|
LOG.debug('new draft')
|
|
1177
1215
|
if (req.target.actives['@Capabilities.InsertRestrictions.Insertable'] === false || req.target.actives['@readonly'])
|
|
1178
|
-
req.reject(405)
|
|
1216
|
+
req.reject({ code: 405, statusCode: 405 })
|
|
1179
1217
|
const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
1180
1218
|
// Only allowed for pseudo draft roots (entities with this action)
|
|
1181
1219
|
if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
|
|
1182
|
-
req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
|
|
1220
|
+
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
|
|
1183
1221
|
|
|
1184
1222
|
_cleanUpOldDrafts(this, req.tenant)
|
|
1185
1223
|
|
|
@@ -1192,9 +1230,14 @@ async function onNew(req) {
|
|
|
1192
1230
|
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
1193
1231
|
])
|
|
1194
1232
|
.where(req.query.INSERT.into.ref[0].where)
|
|
1195
|
-
if (!rootData) req.reject(404)
|
|
1233
|
+
if (!rootData) req.reject({ code: 404, statusCode: 404 })
|
|
1196
1234
|
if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1197
|
-
req.reject(
|
|
1235
|
+
req.reject({
|
|
1236
|
+
code: 403,
|
|
1237
|
+
statusCode: 403,
|
|
1238
|
+
message: 'DRAFT_LOCKED_BY_ANOTHER_USER',
|
|
1239
|
+
args: [rootData.DraftAdministrativeData.InProcessByUser]
|
|
1240
|
+
})
|
|
1198
1241
|
DraftUUID = rootData.DraftAdministrativeData_DraftUUID
|
|
1199
1242
|
}
|
|
1200
1243
|
const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
|
|
@@ -1251,7 +1294,11 @@ async function onEdit(req) {
|
|
|
1251
1294
|
// use symbol for _draftParams
|
|
1252
1295
|
const draftParams = req.query[DRAFT_PARAMS]
|
|
1253
1296
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
|
|
1254
|
-
req.reject(
|
|
1297
|
+
req.reject({
|
|
1298
|
+
code: 400,
|
|
1299
|
+
statusCode: 400,
|
|
1300
|
+
message: 'Action "draftEdit" can only be called on the root active entity'
|
|
1301
|
+
})
|
|
1255
1302
|
}
|
|
1256
1303
|
|
|
1257
1304
|
if (
|
|
@@ -1259,10 +1306,10 @@ async function onEdit(req) {
|
|
|
1259
1306
|
req.target['@insertonly'] ||
|
|
1260
1307
|
req.target['@readonly']
|
|
1261
1308
|
) {
|
|
1262
|
-
req.reject(405)
|
|
1309
|
+
req.reject({ code: 405, statusCode: 405 })
|
|
1263
1310
|
}
|
|
1264
1311
|
|
|
1265
|
-
if (draftParams.IsActiveEntity !== true) req.reject(400)
|
|
1312
|
+
if (draftParams.IsActiveEntity !== true) req.reject({ code: 400, statusCode: 400 })
|
|
1266
1313
|
|
|
1267
1314
|
_cleanUpOldDrafts(this, req.tenant)
|
|
1268
1315
|
|
|
@@ -1337,8 +1384,8 @@ async function onEdit(req) {
|
|
|
1337
1384
|
await this.run(activeLockCQN)
|
|
1338
1385
|
} catch (e) {
|
|
1339
1386
|
const draft = await this.run(existingDraft)
|
|
1340
|
-
if (draft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
1341
|
-
req.reject(409, 'ENTITY_LOCKED')
|
|
1387
|
+
if (draft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
1388
|
+
req.reject({ code: 409, statusCode: 409, message: 'ENTITY_LOCKED' })
|
|
1342
1389
|
}
|
|
1343
1390
|
|
|
1344
1391
|
const cqns = [
|
|
@@ -1366,12 +1413,12 @@ async function onEdit(req) {
|
|
|
1366
1413
|
])
|
|
1367
1414
|
}
|
|
1368
1415
|
|
|
1369
|
-
if (!res) req.reject(404)
|
|
1416
|
+
if (!res) req.reject({ code: 404, statusCode: 404 })
|
|
1370
1417
|
const preserveChanges = req.data?.PreserveChanges
|
|
1371
1418
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
1372
1419
|
|
|
1373
1420
|
if (draft) {
|
|
1374
|
-
if (inProcessByUser || preserveChanges) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
1421
|
+
if (inProcessByUser || preserveChanges) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
1375
1422
|
const keys = {}
|
|
1376
1423
|
for (const key in req.target.drafts.keys) keys[key] = res[key]
|
|
1377
1424
|
await _promiseAll([
|
|
@@ -1405,11 +1452,20 @@ async function onEdit(req) {
|
|
|
1405
1452
|
// REVISIT: needs reworking for new adapter, especially re $batch
|
|
1406
1453
|
if (req._?.odataRes) {
|
|
1407
1454
|
req._?.odataRes?.setStatusCode(201, { overwrite: true })
|
|
1408
|
-
} else if (req.
|
|
1409
|
-
req.
|
|
1455
|
+
} else if (req.res) {
|
|
1456
|
+
req.res.status(201)
|
|
1410
1457
|
}
|
|
1411
1458
|
|
|
1412
|
-
|
|
1459
|
+
if (cds.env.features.odata_new_adapter) {
|
|
1460
|
+
const read_result = await _readAfterDraftAction.bind(this)({ req, payload: res, action: 'draftEdit' })
|
|
1461
|
+
req.res.set(
|
|
1462
|
+
'location',
|
|
1463
|
+
'../' + calculateLocationHeader(req.target, this, read_result || { ...res, IsActiveEntity: false })
|
|
1464
|
+
)
|
|
1465
|
+
return read_result
|
|
1466
|
+
} else {
|
|
1467
|
+
return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
|
|
1468
|
+
}
|
|
1413
1469
|
}
|
|
1414
1470
|
|
|
1415
1471
|
async function onCancel(req) {
|
|
@@ -1430,7 +1486,7 @@ async function onCancel(req) {
|
|
|
1430
1486
|
if (draft) {
|
|
1431
1487
|
const processByUser = draft.DraftAdministrativeData?.InProcessByUser
|
|
1432
1488
|
if (processByUser && processByUser !== cds.context.user.id)
|
|
1433
|
-
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [processByUser])
|
|
1489
|
+
req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [processByUser] })
|
|
1434
1490
|
}
|
|
1435
1491
|
const queries = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }), req.data)]
|
|
1436
1492
|
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
@@ -1457,23 +1513,72 @@ async function onPrepare(req) {
|
|
|
1457
1513
|
LOG.debug('prepare draft')
|
|
1458
1514
|
const draftParams = req.query[DRAFT_PARAMS]
|
|
1459
1515
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
|
|
1460
|
-
req.reject(
|
|
1516
|
+
req.reject({
|
|
1517
|
+
code: 400,
|
|
1518
|
+
statusCode: 400,
|
|
1519
|
+
message: 'Action "draftPrepare" can only be called on the root draft entity'
|
|
1520
|
+
})
|
|
1461
1521
|
}
|
|
1462
1522
|
const where = req.query.SELECT.from.ref[0].where
|
|
1463
1523
|
|
|
1464
1524
|
const draftQuery = SELECT.one
|
|
1465
1525
|
.from(req.target, d => {
|
|
1466
|
-
d.DraftAdministrativeData(a => a.InProcessByUser)
|
|
1526
|
+
d`.*`, d.DraftAdministrativeData(a => a.InProcessByUser)
|
|
1467
1527
|
})
|
|
1468
|
-
.columns(entity_keys(req.target))
|
|
1469
1528
|
.where(where)
|
|
1470
1529
|
draftQuery[DRAFT_PARAMS] = draftParams
|
|
1471
1530
|
const data = await draftQuery
|
|
1472
|
-
if (!data) req.reject(404)
|
|
1531
|
+
if (!data) req.reject({ code: 404, statusCode: 404 })
|
|
1473
1532
|
if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1474
|
-
req.reject(
|
|
1533
|
+
req.reject({
|
|
1534
|
+
code: 403,
|
|
1535
|
+
statusCode: 403,
|
|
1536
|
+
message: 'DRAFT_LOCKED_BY_ANOTHER_USER',
|
|
1537
|
+
args: [data.DraftAdministrativeData?.InProcessByUser]
|
|
1538
|
+
})
|
|
1475
1539
|
delete data.DraftAdministrativeData
|
|
1476
|
-
|
|
1540
|
+
// result must not include DraftAdministrativeData_DraftUUID for plain v4 usage, however required for odata-v2
|
|
1541
|
+
if (data && req.headers?.['x-cds-odata-version'] !== 'v2') {
|
|
1542
|
+
delete data.DraftAdministrativeData_DraftUUID
|
|
1543
|
+
}
|
|
1544
|
+
return { ...data, IsActiveEntity: false, HasDraftEntity: false, HasActiveEntity: data.HasActiveEntity || false }
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const _readAfterDraftAction = async function ({ req, payload, action }) {
|
|
1548
|
+
const entity = action === 'draftActivate' ? req.target : req.target.drafts
|
|
1549
|
+
|
|
1550
|
+
// read after write with query options
|
|
1551
|
+
const keys = {}
|
|
1552
|
+
entity.keys.forEach(key => {
|
|
1553
|
+
if (key.name === 'IsActiveEntity' || key.isAssociation || key.virtual) return
|
|
1554
|
+
keys[key.name] = payload[key.name]
|
|
1555
|
+
})
|
|
1556
|
+
const read = SELECT.one.from(entity, keys)
|
|
1557
|
+
if (req.req.query.$select || req.req.query.$expand) {
|
|
1558
|
+
const queryOptions = []
|
|
1559
|
+
if (req.req.query.$select) queryOptions.push(`$select=${req.req.query.$select}`)
|
|
1560
|
+
if (req.req.query.$expand) queryOptions.push(`$expand=${req.req.query.$expand}`)
|
|
1561
|
+
read.columns(cds.odata.parse(`/X?${queryOptions.join('&')}`).SELECT.columns)
|
|
1562
|
+
// ensure keys are always selected
|
|
1563
|
+
Object.keys(keys).forEach(key => {
|
|
1564
|
+
if (!read.SELECT.columns.some(c => c.ref?.[0] === key)) read.SELECT.columns.push({ ref: [key] })
|
|
1565
|
+
})
|
|
1566
|
+
// also ensure selection of etag columns
|
|
1567
|
+
addEtagColumns(read.SELECT.columns, entity)
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
try {
|
|
1571
|
+
const read_result = await this.run(read)
|
|
1572
|
+
// result must not include DraftAdministrativeData_DraftUUID for plain v4 usage, however required for odata-v2
|
|
1573
|
+
if (read_result && req.headers?.['x-cds-odata-version'] !== 'v2') {
|
|
1574
|
+
delete read_result.DraftAdministrativeData_DraftUUID
|
|
1575
|
+
}
|
|
1576
|
+
return read_result
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
if (!(Number(err.code) in { 401: 1, 403: 1, 404: 1, 405: 1 })) throw err
|
|
1579
|
+
// it's important to return null if one of the above accepted errors occurs
|
|
1580
|
+
return null
|
|
1581
|
+
}
|
|
1477
1582
|
}
|
|
1478
1583
|
|
|
1479
1584
|
module.exports = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { Object_keys } = cds.utils
|
|
3
3
|
const { UPDATE, SELECT } = cds.ql
|
|
4
|
-
const { getColumns } = require('../../
|
|
4
|
+
const { getColumns } = require('../../common/utils/columns')
|
|
5
5
|
const { ensureNoDraftsSuffix, ensureDraftsSuffix, ensureUnlocalized } = require('../../common/utils/draft')
|
|
6
6
|
const getTemplate = require('../../common/utils/template')
|
|
7
7
|
|
|
@@ -404,10 +404,14 @@ async function executeSelectStreamCQN({ model, query, dbc, user, locale, txTimes
|
|
|
404
404
|
if (result.length === 0) return
|
|
405
405
|
|
|
406
406
|
if (cds.env.features.stream_compat) {
|
|
407
|
-
const
|
|
407
|
+
const res = _convertNames(result[0], query.SELECT?.columns)
|
|
408
|
+
let [key, val] = Object.entries(res)[0]
|
|
408
409
|
if (val === null) return null
|
|
409
410
|
|
|
410
|
-
|
|
411
|
+
res.value = val
|
|
412
|
+
delete res[key]
|
|
413
|
+
|
|
414
|
+
return res
|
|
411
415
|
} else {
|
|
412
416
|
return dbc.name === 'hdb' ? result[0] : _convertNames(result[0], query.SELECT?.columns)
|
|
413
417
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
|
-
const { computeColumnsToBeSearched } = require('../
|
|
2
|
+
const { computeColumnsToBeSearched } = require('../common/utils/columns')
|
|
3
3
|
const searchToLike = require('../common/utils/searchToLike')
|
|
4
4
|
const { isContainsPredicateSupported, search2Contains } = require('./search2Contains')
|
|
5
5
|
const { addAliasToExpression } = require('../db/utils/generateAliases')
|
|
@@ -11,7 +11,6 @@ const _isAll = a => a && a.includes('all')
|
|
|
11
11
|
class EndpointRegistry {
|
|
12
12
|
constructor(basePath, LOG) {
|
|
13
13
|
const deployPath = basePath + '/deploy'
|
|
14
|
-
const paths = [basePath, deployPath]
|
|
15
14
|
this.webhookCallbacks = new Map()
|
|
16
15
|
this.deployCallbacks = new Map()
|
|
17
16
|
if (isSecured()) {
|
|
@@ -60,7 +59,7 @@ class EndpointRegistry {
|
|
|
60
59
|
cds.app.options(basePath, (req, res) => {
|
|
61
60
|
try {
|
|
62
61
|
if (isSecured() && !req.user.is('emcallback')) return res.sendStatus(403)
|
|
63
|
-
res.set('
|
|
62
|
+
res.set('webhook-allowed-origin', req.headers['webhook-request-origin'])
|
|
64
63
|
res.sendStatus(200)
|
|
65
64
|
} catch (error) {
|
|
66
65
|
res.sendStatus(500)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
4
|
+
const express = require('express')
|
|
5
|
+
const https = require('https')
|
|
6
|
+
const crypto = require('crypto')
|
|
7
|
+
|
|
8
|
+
async function request(options, data) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const req = https.request(options, res => {
|
|
11
|
+
const chunks = []
|
|
12
|
+
res.on('data', chunk => {
|
|
13
|
+
chunks.push(chunk)
|
|
14
|
+
})
|
|
15
|
+
res.on('end', () => {
|
|
16
|
+
const response = {
|
|
17
|
+
statusCode: res.statusCode,
|
|
18
|
+
headers: res.headers,
|
|
19
|
+
body: Buffer.concat(chunks).toString()
|
|
20
|
+
}
|
|
21
|
+
if (res.statusCode > 299) {
|
|
22
|
+
reject({ message: response.body })
|
|
23
|
+
} else {
|
|
24
|
+
resolve(response)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
req.on('error', error => {
|
|
29
|
+
reject(error)
|
|
30
|
+
})
|
|
31
|
+
if (data) {
|
|
32
|
+
req.write(JSON.stringify(data))
|
|
33
|
+
}
|
|
34
|
+
req.end()
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _validateCertificate(req, res, next) {
|
|
39
|
+
this.LOG.debug('event broker trying to authenticate via mTLS')
|
|
40
|
+
|
|
41
|
+
if (req.headers['x-ssl-client-verify'] !== '0') {
|
|
42
|
+
this.LOG.info('cf did not validate client certificate.')
|
|
43
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!req.headers['x-forwarded-client-cert']) {
|
|
47
|
+
this.LOG.info('no certificate in xfcc header.')
|
|
48
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const bindingCert = new crypto.X509Certificate(this.options.credentials.certificate).toLegacyObject()
|
|
52
|
+
const clientCert = new crypto.X509Certificate(
|
|
53
|
+
`-----BEGIN CERTIFICATE-----\n${req.headers['x-forwarded-client-cert']}\n-----END CERTIFICATE-----`
|
|
54
|
+
).toLegacyObject()
|
|
55
|
+
|
|
56
|
+
const cfSubject = Buffer.from(req.headers['x-ssl-client-subject-cn'], 'base64').toString()
|
|
57
|
+
if (bindingCert.subject.CN !== clientCert.subject.CN || bindingCert.subject.CN !== cfSubject) {
|
|
58
|
+
this.LOG.info('certificate subject does not match')
|
|
59
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
60
|
+
}
|
|
61
|
+
this.LOG.debug('incoming Subject CN is valid.')
|
|
62
|
+
|
|
63
|
+
if (bindingCert.issuer.CN !== clientCert.issuer.CN) {
|
|
64
|
+
this.LOG.info('Certificate issuer subject does not match')
|
|
65
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
66
|
+
}
|
|
67
|
+
this.LOG.debug('incoming issuer subject CN is valid.')
|
|
68
|
+
|
|
69
|
+
if (bindingCert.issuer.O !== clientCert.issuer.O) {
|
|
70
|
+
this.LOG.info('Certificate issuer org does not match')
|
|
71
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
72
|
+
}
|
|
73
|
+
this.LOG.debug('incoming Issuer Org is valid.')
|
|
74
|
+
|
|
75
|
+
if (bindingCert.issuer.OU !== clientCert.issuer.OU) {
|
|
76
|
+
this.LOG.info('certificate issuer OU does not match')
|
|
77
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
78
|
+
}
|
|
79
|
+
this.LOG.debug('certificate issuer OU is valid.')
|
|
80
|
+
|
|
81
|
+
const valid_from = new Date(clientCert.valid_from)
|
|
82
|
+
const valid_to = new Date(clientCert.valid_to)
|
|
83
|
+
const now = new Date(Date.now())
|
|
84
|
+
if (valid_from <= now && valid_to >= now) {
|
|
85
|
+
this.LOG.debug('certificate validation completed')
|
|
86
|
+
next()
|
|
87
|
+
} else {
|
|
88
|
+
this.LOG.error('Certificate expired')
|
|
89
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// TODO: seems unused
|
|
94
|
+
function _checkAppDomains() {
|
|
95
|
+
const pattern = /.*\.cert\.cfapps\..*\.hana\.ondemand\.com/
|
|
96
|
+
const uris = JSON.parse(process.env.VCAP_APPLICATION).application_uris
|
|
97
|
+
const matchFound = uris.some(uri => pattern.test(uri))
|
|
98
|
+
if (matchFound)
|
|
99
|
+
this.LOG.warn(
|
|
100
|
+
`*.cert.cfapps.*.hana.ondemand.com domain is in use, this is not recommended in production! Please use 'mesh.cf.<region>.hana.ondemand.com' instead!`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let instantiated = false
|
|
105
|
+
|
|
106
|
+
class EventBroker extends cds.MessagingService {
|
|
107
|
+
async init() {
|
|
108
|
+
// TODO: Only needed if there are subscriptions
|
|
109
|
+
if (instantiated)
|
|
110
|
+
throw new Error('Event Broker service must be a singleton service, you cannot have more than one instance.')
|
|
111
|
+
instantiated = true
|
|
112
|
+
await super.init()
|
|
113
|
+
cds.once('listening', () => {
|
|
114
|
+
this.startListening()
|
|
115
|
+
})
|
|
116
|
+
this.agent = this.getAgent()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getAgent() {
|
|
120
|
+
try {
|
|
121
|
+
if (this.options.x509.certPath && this.options.x509.pkeyPath) {
|
|
122
|
+
return new https.Agent({
|
|
123
|
+
cert: cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.certPath)),
|
|
124
|
+
key: cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.pkeyPath))
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (this.LOG) this.LOG.error('GetCredentials', { error: error.message })
|
|
129
|
+
throw error
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async handle(msg) {
|
|
134
|
+
if (msg.inbound) return super.handle(msg)
|
|
135
|
+
const _msg = this.message4(msg)
|
|
136
|
+
await this.emitToEventBroker(_msg)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async startListening() {
|
|
140
|
+
if (!this._listenToAll.value && !this.subscribedTopics.size) return
|
|
141
|
+
await this.registerWebhookEndpoints()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async emitToEventBroker(msg) {
|
|
145
|
+
// TODO: CSN definition probably not needed, just in case...
|
|
146
|
+
// See if there's a CSN entry for that event
|
|
147
|
+
// const found = cds?.model.definitions[topicOrEvent]
|
|
148
|
+
// if (found) return found // case for fully-qualified event name
|
|
149
|
+
// for (const def in cds.model?.definitions) {
|
|
150
|
+
// const definition = cds.model.definitions[def]
|
|
151
|
+
// if (definition['@topic'] === topicOrEvent) return definition
|
|
152
|
+
// }
|
|
153
|
+
|
|
154
|
+
// TODO: What if we're in single tenant variant?
|
|
155
|
+
try {
|
|
156
|
+
const ceSource = `${this.options.credentials.ceSource[0]}/${cds.context.tenant}`
|
|
157
|
+
const hostname = this.options.credentials.eventing.http.x509.url.replace(/^https?:\/\//, '')
|
|
158
|
+
// TODO Cloud Events Handler CAP
|
|
159
|
+
const options = {
|
|
160
|
+
hostname: hostname,
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'ce-id': cds.utils.uuid(),
|
|
164
|
+
'ce-source': ceSource,
|
|
165
|
+
'ce-type': msg.event,
|
|
166
|
+
'ce-specversion': '1.0',
|
|
167
|
+
'Content-Type': 'application/json'
|
|
168
|
+
},
|
|
169
|
+
agent: this.agent
|
|
170
|
+
}
|
|
171
|
+
this.LOG.debug('HTTP headers:', JSON.stringify(options.headers))
|
|
172
|
+
this.LOG.debug('HTTP body:', JSON.stringify(msg.data))
|
|
173
|
+
await request(options, msg.data) // TODO: fetch does not work with mTLS as of today, requires another module. see https://github.com/nodejs/node/issues/48977
|
|
174
|
+
if (this.LOG._info) this.LOG.info('Emit', { topic: msg.event })
|
|
175
|
+
} catch (e) {
|
|
176
|
+
this.LOG.error('Emit failed:', e.message)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async registerWebhookEndpoints() {
|
|
181
|
+
const webhookBasePath = this.options.webhookPath || '/-/cds/event-broker/webhook'
|
|
182
|
+
cds.app.post(webhookBasePath, _validateCertificate.bind(this))
|
|
183
|
+
cds.app.post(webhookBasePath, express.json())
|
|
184
|
+
cds.app.post(webhookBasePath, this.onEventReceived.bind(this))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async onEventReceived(req, res) {
|
|
188
|
+
try {
|
|
189
|
+
const event = req.headers['ce-type'] // TG27: type contains namespace, so there's no collision
|
|
190
|
+
const tenant = req.headers['ce-sapconsumertenant']
|
|
191
|
+
const msg = {
|
|
192
|
+
inbound: true,
|
|
193
|
+
event,
|
|
194
|
+
tenant,
|
|
195
|
+
data: req.body ? req.body : undefined,
|
|
196
|
+
headers: req.headers
|
|
197
|
+
}
|
|
198
|
+
cds.context = { user: cds.User.privileged }
|
|
199
|
+
if (tenant) cds.context.tenant = tenant // TODO: In single tenant case, we don't need a tenant
|
|
200
|
+
const tx = await this.tx()
|
|
201
|
+
await tx.emit(msg)
|
|
202
|
+
this.LOG.debug('Event processed successfully.')
|
|
203
|
+
return res.status(200).json({ message: 'OK' })
|
|
204
|
+
} catch (e) {
|
|
205
|
+
this.LOG.error('ERROR during inbound event processing:', e) // TODO: How does Event Broker do error handling?
|
|
206
|
+
res.status(500).json({ message: 'Internal Server Error!' })
|
|
207
|
+
throw e
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = EventBroker
|