@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.
Files changed (137) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/_i18n/i18n_ar.properties +3 -0
  3. package/_i18n/i18n_cs.properties +3 -0
  4. package/_i18n/i18n_da.properties +3 -0
  5. package/_i18n/i18n_es_MX.properties +3 -0
  6. package/_i18n/i18n_fi.properties +3 -0
  7. package/_i18n/i18n_hu.properties +6 -0
  8. package/_i18n/i18n_ko.properties +3 -0
  9. package/_i18n/i18n_ms.properties +3 -0
  10. package/_i18n/i18n_nl.properties +3 -0
  11. package/_i18n/i18n_no.properties +3 -0
  12. package/_i18n/i18n_ro.properties +3 -0
  13. package/_i18n/i18n_sv.properties +3 -0
  14. package/_i18n/i18n_th.properties +3 -0
  15. package/_i18n/i18n_tr.properties +6 -0
  16. package/_i18n/i18n_zh_TW.properties +3 -0
  17. package/bin/serve.js +5 -5
  18. package/lib/auth/basic-auth.js +1 -1
  19. package/lib/compile/cdsc.js +33 -6
  20. package/lib/compile/etc/_localized.js +14 -7
  21. package/lib/compile/for/lean_drafts.js +9 -0
  22. package/lib/compile/to/edm-files.js +116 -0
  23. package/lib/compile/to/edm.js +8 -1
  24. package/lib/compile/to/hdbtabledata.js +3 -3
  25. package/lib/compile/to/sql.js +4 -2
  26. package/lib/compile/to/yaml.js +22 -21
  27. package/lib/dbs/cds-deploy.js +5 -6
  28. package/lib/env/cds-env.js +7 -0
  29. package/lib/env/cds-requires.js +20 -1
  30. package/lib/env/defaults.js +21 -5
  31. package/lib/env/schemas/cds-package.js +1 -1
  32. package/lib/env/schemas/cds-rc.js +85 -4
  33. package/lib/index.js +1 -1
  34. package/lib/linked/classes.js +2 -2
  35. package/lib/linked/entities.js +10 -0
  36. package/lib/linked/models.js +1 -1
  37. package/lib/plugins.js +1 -1
  38. package/lib/ql/INSERT.js +17 -3
  39. package/lib/ql/Query.js +4 -0
  40. package/lib/ql/infer.js +1 -1
  41. package/lib/req/request.js +1 -1
  42. package/lib/srv/cds-serve.js +1 -0
  43. package/lib/srv/middlewares/cds-context.js +1 -1
  44. package/lib/srv/protocols/odata-v4.js +5 -6
  45. package/lib/srv/srv-models.js +9 -2
  46. package/lib/utils/cds-test.js +2 -0
  47. package/lib/utils/cds-utils.js +9 -4
  48. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
  53. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
  64. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
  65. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
  66. package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
  67. package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
  68. package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
  69. package/libx/_runtime/common/generic/auth/index.js +2 -0
  70. package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
  71. package/libx/_runtime/common/generic/auth/restrict.js +6 -5
  72. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  73. package/libx/_runtime/common/generic/crud.js +5 -8
  74. package/libx/_runtime/common/generic/etag.js +8 -6
  75. package/libx/_runtime/common/generic/sorting.js +2 -2
  76. package/libx/_runtime/common/i18n/messages.properties +1 -0
  77. package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
  78. package/libx/_runtime/common/utils/compareJson.js +274 -0
  79. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  80. package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
  81. package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
  82. package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
  83. package/libx/_runtime/common/utils/resolveView.js +0 -16
  84. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  85. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  86. package/libx/_runtime/common/utils/streamProp.js +9 -2
  87. package/libx/_runtime/common/utils/ucsn.js +1 -1
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  89. package/libx/_runtime/db/generic/rewrite.js +7 -13
  90. package/libx/_runtime/fiori/generic/activate.js +1 -1
  91. package/libx/_runtime/fiori/generic/edit.js +1 -1
  92. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  93. package/libx/_runtime/fiori/lean-draft.js +151 -46
  94. package/libx/_runtime/fiori/utils/handler.js +1 -1
  95. package/libx/_runtime/hana/execute.js +6 -2
  96. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  97. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  98. package/libx/_runtime/messaging/event-broker.js +212 -0
  99. package/libx/_runtime/remote/Service.js +9 -32
  100. package/libx/_runtime/remote/utils/client.js +13 -21
  101. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
  102. package/libx/_runtime/sqlite/execute.js +8 -3
  103. package/libx/_runtime/ucl/Service.js +259 -0
  104. package/libx/common/assert/index.js +6 -11
  105. package/libx/common/assert/validation.js +6 -1
  106. package/libx/odata/index.js +47 -25
  107. package/libx/odata/middleware/batch.js +8 -7
  108. package/libx/odata/middleware/create.js +42 -16
  109. package/libx/odata/middleware/delete.js +18 -11
  110. package/libx/odata/middleware/metadata.js +15 -14
  111. package/libx/odata/middleware/operation.js +30 -40
  112. package/libx/odata/middleware/parse.js +2 -3
  113. package/libx/odata/middleware/read.js +59 -52
  114. package/libx/odata/middleware/service-document.js +7 -7
  115. package/libx/odata/middleware/stream.js +26 -24
  116. package/libx/odata/middleware/update.js +53 -92
  117. package/libx/odata/parse/afterburner.js +45 -47
  118. package/libx/odata/parse/grammar.peggy +3 -3
  119. package/libx/odata/parse/multipartToJson.js +10 -22
  120. package/libx/odata/parse/parser.js +1 -1
  121. package/libx/odata/utils/etag.js +13 -0
  122. package/libx/odata/utils/handler.js +120 -0
  123. package/libx/odata/utils/index.js +15 -2
  124. package/libx/odata/utils/metaInfo.js +410 -0
  125. package/libx/odata/utils/path.js +5 -2
  126. package/libx/odata/utils/readAfterWrite.js +23 -0
  127. package/libx/odata/utils/result.js +4 -5
  128. package/libx/rest/RestAdapter.js +4 -13
  129. package/libx/rest/middleware/parse.js +40 -7
  130. package/package.json +1 -1
  131. package/server.js +2 -1
  132. package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
  133. package/libx/_runtime/common/utils/thenable.js +0 -51
  134. package/libx/_runtime/rest/service.js +0 -2
  135. package/libx/odata/parse/parseToCqn.js +0 -39
  136. package/libx/rest/middleware/input.js +0 -54
  137. 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 msg =
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, msg)
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(400, 'Action "draftActivate" can only be called on the root draft entity')
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) req.reject(_etagValidationType ? 412 : { code: 'DRAFT_NOT_EXISTING', status: 404 })
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(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
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.http?.res) {
473
- req.http.res.status(201)
487
+ } else if (req.res) {
488
+ req.res.status(201)
474
489
  }
475
490
  }
476
491
 
477
- return Object.assign(result, { IsActiveEntity: true })
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) req.reject(403)
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(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
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
- const ref = q.SELECT?.from.ref || q.UPDATE?.entity.ref || q.INSERT?.into.ref || q.DELETE?.from.ref
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(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [rootData.DraftAdministrativeData.InProcessByUser])
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(400, 'Action "draftEdit" can only be called on the root active entity')
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.http?.res) {
1409
- req.http.res.status(201)
1455
+ } else if (req.res) {
1456
+ req.res.status(201)
1410
1457
  }
1411
1458
 
1412
- return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
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(400, 'Action "draftPrepare" can only be called on the root draft entity')
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(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [data.DraftAdministrativeData?.InProcessByUser])
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
- return { ...data, IsActiveEntity: false }
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('../../cds-services/services/utils/columns')
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 val = Object.values(result[0])[0]
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
- return { value: val }
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('../cds-services/services/utils/columns')
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('WebHook-Allowed-Origin', req.headers['webhook-request-origin'])
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