@sap/cds 5.8.4 → 5.9.2

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 (248) hide show
  1. package/CHANGELOG.md +198 -77
  2. package/app/fiori/preview.js +16 -11
  3. package/app/fiori/routes.js +15 -8
  4. package/app/index.js +1 -1
  5. package/bin/build/buildTaskFactory.js +3 -3
  6. package/bin/build/buildTaskProviderFactory.js +1 -1
  7. package/bin/build/constants.js +1 -1
  8. package/bin/build/provider/buildTaskHandlerEdmx.js +12 -7
  9. package/bin/build/provider/buildTaskHandlerInternal.js +1 -1
  10. package/bin/build/provider/buildTaskProviderInternal.js +8 -2
  11. package/bin/build/provider/hana/2migration.js +27 -24
  12. package/bin/build/provider/hana/index.js +17 -18
  13. package/bin/build/provider/hana/migrationtable.js +9 -10
  14. package/bin/build/provider/java-cf/index.js +4 -5
  15. package/bin/build/provider/node-cf/index.js +99 -6
  16. package/bin/cds.js +17 -18
  17. package/bin/deploy/to-hana/cfUtil.js +16 -19
  18. package/bin/deploy/to-hana/hana.js +7 -24
  19. package/bin/deploy/to-hana/hdiDeployUtil.js +8 -4
  20. package/bin/mtx/in-cds.js +2 -2
  21. package/bin/serve.js +10 -3
  22. package/bin/utils/modules.js +7 -0
  23. package/bin/version.js +56 -3
  24. package/lib/compile/cdsc.js +7 -2
  25. package/lib/compile/etc/_localized.js +37 -25
  26. package/lib/compile/etc/csv.js +8 -8
  27. package/lib/compile/for/drafts.js +9 -0
  28. package/lib/compile/for/java.js +16 -0
  29. package/lib/compile/for/nodejs.js +12 -0
  30. package/lib/compile/index.js +3 -0
  31. package/lib/compile/minify.js +16 -2
  32. package/lib/compile/parse.js +2 -2
  33. package/lib/compile/resolve.js +35 -18
  34. package/lib/compile/to/json.js +3 -1
  35. package/lib/compile/to/sql.js +2 -2
  36. package/lib/compile/to/srvinfo.js +4 -2
  37. package/lib/connect/bindings.js +1 -1
  38. package/lib/connect/index.js +3 -4
  39. package/lib/core/entities.js +15 -14
  40. package/lib/core/index.js +39 -36
  41. package/lib/core/reflect.js +4 -2
  42. package/lib/deploy.js +114 -127
  43. package/lib/env/defaults.js +1 -0
  44. package/lib/env/index.js +165 -165
  45. package/lib/env/presets.js +1 -0
  46. package/lib/env/requires.js +121 -50
  47. package/lib/index.js +2 -0
  48. package/lib/log/format/kibana.js +2 -2
  49. package/lib/ql/SELECT.js +10 -0
  50. package/lib/ql/parse.js +1 -0
  51. package/lib/req/cds-context.js +4 -1
  52. package/lib/req/context.js +50 -56
  53. package/lib/req/event.js +1 -6
  54. package/lib/req/locale.js +6 -5
  55. package/lib/req/request.js +2 -0
  56. package/lib/req/user.js +7 -5
  57. package/lib/serve/Service-api.js +10 -7
  58. package/lib/serve/Service-dispatch.js +9 -11
  59. package/lib/serve/Service-methods.js +30 -41
  60. package/lib/serve/Transaction.js +10 -7
  61. package/lib/serve/adapters.js +11 -9
  62. package/lib/serve/factory.js +14 -9
  63. package/lib/serve/index.js +28 -15
  64. package/lib/utils/data.js +1 -1
  65. package/lib/utils/index.js +27 -30
  66. package/lib/utils/resources/index.js +101 -0
  67. package/lib/utils/resources/tar.js +71 -0
  68. package/lib/utils/resources/utils.js +11 -0
  69. package/libx/_runtime/audit/Service.js +36 -39
  70. package/libx/_runtime/audit/generic/personal/access.js +3 -4
  71. package/libx/_runtime/audit/generic/personal/modification.js +3 -4
  72. package/libx/_runtime/audit/utils/v2.js +1 -2
  73. package/libx/_runtime/auth/index.js +126 -84
  74. package/libx/_runtime/auth/strategies/JWT.js +12 -19
  75. package/libx/_runtime/auth/strategies/dummy.js +1 -5
  76. package/libx/_runtime/auth/strategies/dwc.js +11 -9
  77. package/libx/_runtime/auth/strategies/mock.js +0 -4
  78. package/libx/_runtime/auth/strategies/{utils/xssec.js → xssecUtils.js} +7 -4
  79. package/libx/_runtime/auth/strategies/xsuaa.js +12 -19
  80. package/libx/_runtime/auth/utils.js +22 -1
  81. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +104 -98
  82. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +8 -3
  83. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  84. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  85. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/language.js +2 -8
  86. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +4 -29
  87. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -1
  88. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +3 -2
  89. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +2 -2
  90. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +4 -6
  91. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +24 -21
  92. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +8 -2
  93. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +2 -0
  94. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -6
  95. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -12
  96. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +33 -9
  97. package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +56 -0
  98. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -2
  99. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +10 -3
  100. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +9 -11
  101. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +6 -3
  102. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +4 -2
  103. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/utils.js +1 -1
  104. package/libx/_runtime/cds-services/adapter/rest/utils/binary.js +1 -1
  105. package/libx/_runtime/cds-services/adapter/rest/utils/key-value-utils.js +2 -3
  106. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +6 -4
  107. package/libx/_runtime/cds-services/adapter/rest/utils/result.js +1 -0
  108. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +8 -5
  109. package/libx/_runtime/cds-services/services/Service.js +40 -0
  110. package/libx/_runtime/cds-services/services/utils/columns.js +4 -3
  111. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -4
  112. package/libx/_runtime/cds-services/services/utils/differ.js +3 -3
  113. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +4 -4
  114. package/libx/_runtime/cds-services/util/assert.js +20 -14
  115. package/libx/_runtime/cds.js +9 -1
  116. package/libx/_runtime/common/aspects/any.js +5 -0
  117. package/libx/_runtime/common/aspects/entity.js +25 -7
  118. package/libx/_runtime/common/aspects/utils.js +2 -2
  119. package/libx/_runtime/common/composition/data.js +6 -0
  120. package/libx/_runtime/common/composition/insert.js +3 -2
  121. package/libx/_runtime/common/composition/tree.js +4 -10
  122. package/libx/_runtime/common/composition/update.js +4 -4
  123. package/libx/_runtime/common/constants/draft.js +29 -26
  124. package/libx/_runtime/common/error/constants.js +2 -2
  125. package/libx/_runtime/common/error/frontend.js +7 -15
  126. package/libx/_runtime/common/generic/auth/capabilities.js +59 -0
  127. package/libx/_runtime/common/generic/auth/constants.js +20 -0
  128. package/libx/_runtime/common/generic/auth/expand.js +54 -0
  129. package/libx/_runtime/common/generic/auth/index.js +32 -0
  130. package/libx/_runtime/common/generic/auth/insertOnly.js +15 -0
  131. package/libx/_runtime/common/generic/auth/readOnly.js +26 -0
  132. package/libx/_runtime/common/generic/auth/requires.js +34 -0
  133. package/libx/_runtime/common/generic/auth/restrict.js +298 -0
  134. package/libx/_runtime/common/generic/auth/restrictions.js +85 -0
  135. package/libx/_runtime/common/generic/auth/utils.js +213 -0
  136. package/libx/_runtime/common/generic/crud.js +8 -6
  137. package/libx/_runtime/common/generic/etag.js +1 -1
  138. package/libx/_runtime/common/generic/input.js +35 -35
  139. package/libx/_runtime/common/generic/sorting.js +2 -3
  140. package/libx/_runtime/common/generic/temporal.js +2 -2
  141. package/libx/_runtime/common/i18n/messages.properties +1 -1
  142. package/libx/_runtime/common/toggles/handler.js +21 -0
  143. package/libx/_runtime/common/utils/copy.js +10 -1
  144. package/libx/_runtime/common/utils/cqn2cqn4sql.js +111 -35
  145. package/libx/_runtime/common/utils/csn.js +63 -1
  146. package/libx/_runtime/common/utils/dollar.js +10 -1
  147. package/libx/_runtime/common/utils/draft.js +46 -7
  148. package/libx/_runtime/common/utils/entityFromCqn.js +13 -9
  149. package/libx/_runtime/common/utils/extensibilityUtils.js +18 -0
  150. package/libx/_runtime/common/utils/foreignKeyPropagations.js +88 -104
  151. package/libx/_runtime/common/utils/generateOnCond.js +4 -1
  152. package/libx/_runtime/common/utils/quotingStyles.js +2 -0
  153. package/libx/_runtime/common/utils/resolveStructured.js +25 -9
  154. package/libx/_runtime/common/utils/resolveView.js +4 -1
  155. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -16
  156. package/libx/_runtime/common/utils/structured.js +33 -37
  157. package/libx/_runtime/common/utils/template.js +17 -8
  158. package/libx/_runtime/common/utils/templateProcessor.js +28 -28
  159. package/libx/_runtime/db/data-conversion/post-processing.js +118 -412
  160. package/libx/_runtime/db/expand/expandCQNToJoin.js +45 -41
  161. package/libx/_runtime/db/expand/rawToExpanded.js +29 -8
  162. package/libx/_runtime/db/generic/index.js +1 -3
  163. package/libx/_runtime/db/generic/input.js +5 -10
  164. package/libx/_runtime/db/generic/rewrite.js +5 -2
  165. package/libx/_runtime/db/generic/structured.js +2 -2
  166. package/libx/_runtime/db/query/delete.js +2 -2
  167. package/libx/_runtime/db/query/insert.js +1 -1
  168. package/libx/_runtime/db/query/update.js +9 -14
  169. package/libx/_runtime/db/sql-builder/CreateBuilder.js +4 -3
  170. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +8 -8
  171. package/libx/_runtime/db/sql-builder/InsertBuilder.js +14 -1
  172. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -2
  173. package/libx/_runtime/db/sql-builder/dataTypes.js +3 -3
  174. package/libx/_runtime/db/utils/columns.js +3 -3
  175. package/libx/_runtime/db/utils/normalizeTimeData.js +2 -2
  176. package/libx/_runtime/db/utils/propagateForeignKeys.js +6 -2
  177. package/libx/_runtime/extensibility/mps/index.js +5 -0
  178. package/libx/_runtime/extensibility/mps/service.js +111 -0
  179. package/libx/_runtime/extensibility/mps/tar.js +42 -0
  180. package/libx/_runtime/extensibility/mps/utils.js +11 -0
  181. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformREAD.js +0 -0
  182. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformRESULT.js +17 -5
  183. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformWRITE.js +1 -0
  184. package/libx/_runtime/extensibility/uiflex/index.js +54 -0
  185. package/libx/_runtime/extensibility/uiflex/service.js +276 -0
  186. package/libx/_runtime/{fiori → extensibility}/uiflex/utils.js +22 -7
  187. package/libx/_runtime/fiori/generic/activate.js +2 -2
  188. package/libx/_runtime/fiori/generic/before.js +4 -4
  189. package/libx/_runtime/fiori/generic/new.js +3 -3
  190. package/libx/_runtime/fiori/generic/patch.js +1 -1
  191. package/libx/_runtime/fiori/generic/read.js +58 -66
  192. package/libx/_runtime/fiori/generic/readOverDraft.js +74 -16
  193. package/libx/_runtime/fiori/utils/handler.js +6 -13
  194. package/libx/_runtime/fiori/utils/where.js +6 -5
  195. package/libx/_runtime/hana/Service.js +4 -10
  196. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +1 -1
  197. package/libx/_runtime/hana/driver.js +2 -2
  198. package/libx/_runtime/hana/execute.js +45 -75
  199. package/libx/_runtime/hana/pool.js +1 -1
  200. package/libx/_runtime/hana/streaming.js +2 -1
  201. package/libx/_runtime/index.js +6 -6
  202. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +5 -21
  203. package/libx/_runtime/messaging/Outbox.js +2 -2
  204. package/libx/_runtime/messaging/common-utils/AMQPClient.js +4 -14
  205. package/libx/_runtime/messaging/common-utils/connections.js +5 -7
  206. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +30 -0
  207. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -1
  208. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +36 -30
  209. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -12
  210. package/libx/_runtime/messaging/enterprise-messaging.js +8 -8
  211. package/libx/_runtime/messaging/file-based.js +5 -5
  212. package/libx/_runtime/messaging/message-queuing.js +14 -12
  213. package/libx/_runtime/messaging/outbox/utils.js +18 -19
  214. package/libx/_runtime/messaging/redis-messaging.js +91 -0
  215. package/libx/_runtime/messaging/service.js +8 -6
  216. package/libx/_runtime/remote/Service.js +44 -8
  217. package/libx/_runtime/remote/utils/client.js +24 -19
  218. package/libx/_runtime/remote/utils/data.js +11 -11
  219. package/libx/_runtime/sqlite/Service.js +6 -9
  220. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +5 -2
  221. package/libx/_runtime/types/api.js +10 -2
  222. package/libx/common/utils/ucsn.js +109 -0
  223. package/libx/gql/resolvers/crud/update.js +5 -0
  224. package/libx/gql/resolvers/parse/ast2cqn/columns.js +3 -1
  225. package/libx/gql/schema/typeDefMap.js +2 -2
  226. package/libx/odata/afterburner.js +110 -16
  227. package/libx/odata/cqn2odata.js +24 -27
  228. package/libx/odata/grammar.pegjs +9 -1
  229. package/libx/odata/parseToCqn.js +39 -0
  230. package/libx/odata/parser.js +1 -1
  231. package/libx/rest/RestAdapter.js +9 -1
  232. package/libx/rest/middleware/input.js +54 -0
  233. package/libx/rest/middleware/operation.js +14 -1
  234. package/libx/rest/middleware/parse.js +11 -7
  235. package/package.json +2 -2
  236. package/server.js +34 -19
  237. package/srv/audit-log.cds +2 -2
  238. package/srv/flex.cds +8 -2
  239. package/srv/flex.js +1 -1
  240. package/srv/mps.cds +23 -0
  241. package/srv/mps.js +1 -0
  242. package/libx/_runtime/auth/strategies/utils/uaa.js +0 -21
  243. package/libx/_runtime/common/generic/auth.js +0 -874
  244. package/libx/_runtime/common/toggles/alpha.js +0 -43
  245. package/libx/_runtime/db/generic/arrayed.js +0 -33
  246. package/libx/_runtime/fiori/uiflex/index.js +0 -35
  247. package/libx/_runtime/fiori/uiflex/service.js +0 -150
  248. package/libx/rest/utils/data.js +0 -60
@@ -1,16 +1,13 @@
1
1
  const cds = require('../../cds.js')
2
- const LOG = cds.log('messaging')
3
2
  const express = require('express')
4
3
  const getTenantInfo = require('./getTenantInfo.js')
5
- const isSecured = () => cds.requires.uaa && cds.requires.uaa.credentials
6
- const { getTenant } = require('../../auth/strategies/utils/xssec.js')
4
+ const isSecured = () => cds.requires.auth && cds.requires.auth.credentials
5
+ const { getTenant } = require('../../auth/strategies/xssecUtils')
7
6
 
8
7
  const _isAll = a => a && a.includes('all')
9
- const _hasScope = (scope, req) =>
10
- req && req.authInfo && req.authInfo.checkLocalScope && req.authInfo.checkLocalScope(scope)
11
8
 
12
9
  class EndpointRegistry {
13
- constructor(basePath) {
10
+ constructor(basePath, LOG) {
14
11
  const deployPath = basePath + '/deploy'
15
12
  const paths = [basePath, deployPath]
16
13
  this.webhookCallbacks = new Map()
@@ -18,11 +15,20 @@ class EndpointRegistry {
18
15
  if (isSecured()) {
19
16
  const JWTStrategy = require('../../auth/strategies/JWT.js')
20
17
  const passport = require('passport')
21
- passport.use(new JWTStrategy(cds.requires.uaa))
18
+ // REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
19
+ // In principle, user-facing endpoints might differ from messaging ones.
20
+ passport.use(new JWTStrategy(cds.requires.auth.credentials))
22
21
  paths.forEach(path => {
23
22
  cds.app.use(path, passport.initialize())
24
23
  cds.app.use(path, passport.authenticate('JWT', { session: false }))
24
+ cds.app.use(path, (req, res, next) => {
25
+ // unsuccessful auth doesn't automatically reject!
26
+ if (!req.user) return res.status(401).json({ message: 'Unauthorized' })
27
+ next()
28
+ })
25
29
  })
30
+ } else if (process.env.NODE_ENV === 'production') {
31
+ LOG.warn('Messaging endpoints not secured')
26
32
  }
27
33
  paths.forEach(path => {
28
34
  cds.app.use(path, express.json({ type: 'application/*+json' }))
@@ -43,7 +49,7 @@ class EndpointRegistry {
43
49
 
44
50
  cds.app.options(basePath, (req, res) => {
45
51
  try {
46
- if (isSecured() && !_hasScope('emcallback', req)) return res.sendStatus(403)
52
+ if (isSecured() && !req.user.is('emcallback')) return res.sendStatus(403)
47
53
  res.set('WebHook-Allowed-Origin', req.headers['webhook-request-origin'])
48
54
  res.sendStatus(200)
49
55
  } catch (error) {
@@ -53,7 +59,7 @@ class EndpointRegistry {
53
59
  LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'POST' })
54
60
  cds.app.post(basePath, (req, res) => {
55
61
  try {
56
- if (isSecured() && !_hasScope('emcallback', req)) return res.sendStatus(403)
62
+ if (isSecured() && !req.user.is('emcallback')) return res.sendStatus(403)
57
63
  const queueName = req.query.q
58
64
  const authInfo = req.authInfo
59
65
  const xAddress = req.headers['x-address']
@@ -83,7 +89,7 @@ class EndpointRegistry {
83
89
  })
84
90
  cds.app.post(deployPath, async (req, res) => {
85
91
  try {
86
- if (isSecured() && !_hasScope('emmanagement', req)) return res.sendStatus(403)
92
+ if (isSecured() && !req.user.is('emmanagement')) return res.sendStatus(403)
87
93
  const tenants = req.body && !_isAll(req.body.tenants) && req.body.tenants
88
94
  const queues = req.body && !_isAll(req.body.queues) && req.body.queues
89
95
  const options = { wipeData: req.body && req.body.wipeData }
@@ -120,9 +126,10 @@ class EndpointRegistry {
120
126
  const registries = new Map()
121
127
 
122
128
  // REVISIT: Use cds mechanism instead of express? -> Need option method and handler for specifica
123
- const registerWebhookEndpoints = (basePath, queueName, cb) => {
129
+ const registerWebhookEndpoints = (basePath, queueName, LOG, cb) => {
124
130
  const registry =
125
- registries.get(basePath) || (registries.set(basePath, new EndpointRegistry(basePath)) && registries.get(basePath))
131
+ registries.get(basePath) ||
132
+ (registries.set(basePath, new EndpointRegistry(basePath, LOG)) && registries.get(basePath))
126
133
  registry.registerWebhookCallback(queueName, cb)
127
134
  }
128
135
 
@@ -10,7 +10,6 @@ const {
10
10
  registerDeployEndpoints,
11
11
  registerWebhookEndpoints
12
12
  } = require('./enterprise-messaging-utils/registerEndpoints.js')
13
- const LOG = cds.log('messaging')
14
13
  const cloudEvents = require('./enterprise-messaging-utils/cloudEvents.js')
15
14
 
16
15
  const BASE_PATH = '/messaging/enterprise-messaging'
@@ -61,12 +60,12 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
61
60
  const management = await this.getManagement(subdomain).waitUntilReady()
62
61
  await management.undeploy()
63
62
  } catch (error) {
64
- LOG._error && LOG.error('Failed to delete messaging artifacts for subdomain', subdomain, '(', error, ')')
63
+ this.LOG.error('Failed to delete messaging artifacts for subdomain', subdomain, '(', error, ')')
65
64
  }
66
65
  return next()
67
66
  })
68
67
  provisioning.on('dependencies', async (req, next) => {
69
- LOG._info && LOG.info('Include Enterprise-Messaging as SaaS dependency')
68
+ this.LOG._info && this.LOG.info('Include Enterprise-Messaging as SaaS dependency')
70
69
  const res = await next()
71
70
  const xsappname = this.options.credentials && this.options.credentials.xsappname
72
71
  if (xsappname) {
@@ -80,7 +79,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
80
79
 
81
80
  startListening() {
82
81
  const doNotDeploy = cds._mtxEnabled && !this.options.deployForProvider
83
- if (doNotDeploy) LOG._info && LOG.info('Skipping deployment of messaging artifacts for provider account')
82
+ if (doNotDeploy) this.LOG._info && this.LOG.info('Skipping deployment of messaging artifacts for provider account')
84
83
  super.startListening({ doNotDeploy })
85
84
  if (!doNotDeploy && this.subscribedTopics.size) {
86
85
  const management = this.getManagement()
@@ -97,7 +96,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
97
96
 
98
97
  async listenToClient(cb) {
99
98
  _checkAppURL(this.optionsApp.appURL)
100
- registerWebhookEndpoints(BASE_PATH, this.queueName, cb)
99
+ registerWebhookEndpoints(BASE_PATH, this.queueName, this.LOG, cb)
101
100
  if (cds._mtxEnabled) {
102
101
  await this.addMTXHandlers()
103
102
  registerDeployEndpoints(BASE_PATH, this.queueName, async (tenantInfo, options) => {
@@ -110,7 +109,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
110
109
  await management.deploy()
111
110
  result.succeeded.push(info.tenant)
112
111
  } catch (error) {
113
- LOG._error && LOG.error('Failed to create messaging artifacts for subdomain', info.subdomain, ':', error)
112
+ this.LOG.error('Failed to create messaging artifacts for subdomain', info.subdomain, ':', error)
114
113
  result.failed.push({ error: error.message, tenant: info.tenant })
115
114
  }
116
115
  })
@@ -147,7 +146,8 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
147
146
  subscribedTopics: this.subscribedTopics,
148
147
  alternativeTopics: this.alternativeTopics,
149
148
  subdomain: _subdomain,
150
- namespace: this.options.credentials && this.options.credentials.namespace
149
+ namespace: this.options.credentials && this.options.credentials.namespace,
150
+ LOG: this.LOG
151
151
  })
152
152
  }
153
153
 
@@ -174,7 +174,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
174
174
  headers: {
175
175
  'x-qos': 1
176
176
  },
177
- attemptInfo: () => LOG._info && LOG.info('Emit', { topic }),
177
+ attemptInfo: () => this.LOG._info && this.LOG.info('Emit', { topic }),
178
178
  errMsg,
179
179
  target: { kind: 'MESSAGE', topic },
180
180
  tokenStore: {}
@@ -1,5 +1,4 @@
1
1
  const cds = require('../cds')
2
- const LOG = cds.log('messaging')
3
2
 
4
3
  const path = require('path')
5
4
  const fs = require('fs').promises
@@ -25,11 +24,11 @@ class FileBasedMessaging extends MessagingService {
25
24
  const e = _msg.event
26
25
  delete _msg.event
27
26
  await this.queued(lock)(this.file)
28
- LOG._debug && LOG.debug('Emit', { topic: e, file: this.file })
27
+ this.LOG._debug && this.LOG.debug('Emit', { topic: e, file: this.file })
29
28
  try {
30
29
  await fs.appendFile(this.file, `\n${e} ${JSON.stringify(_msg)}`)
31
30
  } catch (e) {
32
- LOG._debug && LOG.debug('Error', e)
31
+ this.LOG._debug && this.LOG.debug('Error', e)
33
32
  } finally {
34
33
  unlock(this.file)
35
34
  }
@@ -53,9 +52,10 @@ class FileBasedMessaging extends MessagingService {
53
52
  if (this.subscribedTopics.has(topic)) {
54
53
  const event = this.subscribedTopics.get(topic)
55
54
  if (!event) return
55
+ // REVISIT: should we use this.dispatch instead of super.emit for inbound messages?
56
56
  super
57
57
  .emit({ event, ...json, inbound: true })
58
- .catch(e => LOG.error('ERROR occured in asynchronous event processing:', e))
58
+ .catch(e => this.LOG.error('ERROR occured in asynchronous event processing:', e))
59
59
  } else other.push(each + '\n')
60
60
  }
61
61
  } catch (e) {
@@ -65,7 +65,7 @@ class FileBasedMessaging extends MessagingService {
65
65
  if (other.length < lines.length) await fs.writeFile(this.file, other.join(''))
66
66
  this.recent = await touched(this.file)
67
67
  } catch (e) {
68
- LOG._debug && LOG.debug(e)
68
+ this.LOG._debug && this.LOG.debug(e)
69
69
  } finally {
70
70
  unlock(this.file)
71
71
  }
@@ -1,18 +1,17 @@
1
- const cds = require('../cds.js')
2
1
  const AMQPWebhookMessaging = require('./AMQPWebhookMessaging')
3
2
  const AMQPClient = require('./common-utils/AMQPClient.js')
4
3
 
5
4
  const optionsMessaging = require('./message-queuing-utils/options-messaging.js')
6
5
  const optionsManagement = require('./message-queuing-utils/options-management.js')
7
6
  const authorizedRequest = require('./common-utils/authorizedRequest')
8
- const LOG = cds.log('messaging')
9
7
 
10
8
  class MQManagement {
11
- constructor({ options, queueConfig, queueName, subscribedTopics }) {
9
+ constructor({ options, queueConfig, queueName, subscribedTopics, LOG }) {
12
10
  this.options = options
13
11
  this.queueConfig = queueConfig
14
12
  this.queueName = queueName
15
13
  this.subscribedTopics = subscribedTopics
14
+ this.LOG = LOG
16
15
  }
17
16
 
18
17
  async getQueue(queueName = this.queueName) {
@@ -21,7 +20,7 @@ class MQManagement {
21
20
  uri: this.options.url,
22
21
  path: `/v1/management/queues/${encodeURIComponent(queueName)}`,
23
22
  oa2: this.options.auth.oauth2,
24
- attemptInfo: () => LOG._info && LOG.info('Get queue', { queue: queueName }),
23
+ attemptInfo: () => this.LOG._info && this.LOG.info('Get queue', { queue: queueName }),
25
24
  errMsg: `Queue "${queueName}" could not be retrieved`,
26
25
  target: { kind: 'QUEUE', queue: queueName },
27
26
  tokenStore: this
@@ -35,7 +34,7 @@ class MQManagement {
35
34
  uri: this.options.url,
36
35
  path: `/v1/management/queues`,
37
36
  oa2: this.options.auth.oauth2,
38
- attemptInfo: () => LOG._info && LOG.info('Get queues'),
37
+ attemptInfo: () => this.LOG._info && this.LOG.info('Get queues'),
39
38
  errMsg: `Queues could not be retrieved`,
40
39
  target: { kind: 'QUEUE' },
41
40
  tokenStore: this
@@ -50,7 +49,7 @@ class MQManagement {
50
49
  path: `/v1/management/queues/${encodeURIComponent(queueName)}`,
51
50
  oa2: this.options.auth.oauth2,
52
51
  dataObj: this.queueConfig,
53
- attemptInfo: () => LOG._info && LOG.info('Create queue', { queue: queueName }),
52
+ attemptInfo: () => this.LOG._info && this.LOG.info('Create queue', { queue: queueName }),
54
53
  errMsg: `Queue "${queueName}" could not be created`,
55
54
  target: { kind: 'QUEUE', queue: queueName },
56
55
  tokenStore: this
@@ -63,7 +62,7 @@ class MQManagement {
63
62
  uri: this.options.url,
64
63
  path: `/v1/management/queues/${encodeURIComponent(queueName)}`,
65
64
  oa2: this.options.auth.oauth2,
66
- attemptInfo: () => LOG._info && LOG.info('Delete queue', { queue: queueName }),
65
+ attemptInfo: () => this.LOG._info && this.LOG.info('Delete queue', { queue: queueName }),
67
66
  errMsg: `Queue "${queueName}" could not be deleted`,
68
67
  target: { kind: 'QUEUE', queue: queueName },
69
68
  tokenStore: this
@@ -76,7 +75,7 @@ class MQManagement {
76
75
  uri: this.options.url,
77
76
  path: `/v1/management/queues/${encodeURIComponent(queueName)}/subscriptions/topics`,
78
77
  oa2: this.options.auth.oauth2,
79
- attemptInfo: () => LOG._info && LOG.info('Get subscriptions', { queue: queueName }),
78
+ attemptInfo: () => this.LOG._info && this.LOG.info('Get subscriptions', { queue: queueName }),
80
79
  errMsg: `Subscriptions for "${queueName}" could not be retrieved`,
81
80
  target: { kind: 'SUBSCRIPTION', queue: queueName },
82
81
  tokenStore: this
@@ -92,7 +91,8 @@ class MQManagement {
92
91
  topicPattern
93
92
  )}`,
94
93
  oa2: this.options.auth.oauth2,
95
- attemptInfo: () => LOG._info && LOG.info('Create subscription', { topic: topicPattern, queue: queueName }),
94
+ attemptInfo: () =>
95
+ this.LOG._info && this.LOG.info('Create subscription', { topic: topicPattern, queue: queueName }),
96
96
  errMsg: `Subscription "${topicPattern}" could not be added to queue "${queueName}"`,
97
97
  target: { kind: 'SUBSCRIPTION', queue: queueName, topic: topicPattern },
98
98
  tokenStore: this
@@ -107,7 +107,8 @@ class MQManagement {
107
107
  topicPattern
108
108
  )}`,
109
109
  oa2: this.options.auth.oauth2,
110
- attemptInfo: () => LOG._info && LOG.info('Delete subscription', { topic: topicPattern, queue: queueName }),
110
+ attemptInfo: () =>
111
+ this.LOG._info && this.LOG.info('Delete subscription', { topic: topicPattern, queue: queueName }),
111
112
  errMsg: `Subscription "${topicPattern}" could not be deleted from queue "${queueName}"`,
112
113
  target: { kind: 'SUBSCRIPTION', queue: queueName, topic: topicPattern },
113
114
  tokenStore: this
@@ -115,7 +116,7 @@ class MQManagement {
115
116
  }
116
117
 
117
118
  async createQueueAndSubscriptions() {
118
- LOG._info && LOG.info(`Create messaging artifacts`)
119
+ this.LOG._info && this.LOG.info(`Create messaging artifacts`)
119
120
  const created = await this.createQueue()
120
121
  if (created && created.statusCode === 200) {
121
122
  // We need to make sure to only keep our own subscriptions
@@ -171,7 +172,8 @@ class MessageQueuing extends AMQPWebhookMessaging {
171
172
  options: _optionsManagement,
172
173
  queueConfig,
173
174
  queueName,
174
- subscribedTopics: this.subscribedTopics
175
+ subscribedTopics: this.subscribedTopics,
176
+ LOG: this.LOG
175
177
  })
176
178
  return this.management
177
179
  }
@@ -21,9 +21,10 @@ const _getMessagesEntity = () => {
21
21
  return messagesEntity
22
22
  }
23
23
 
24
- // REVISIT: Is there a better way?
24
+ // REVISIT: Is this always a reliable way to identify the provider tenant?
25
+ // Are there scenarios where the credentials have a different format?
25
26
  const _isProviderTenant = tenant =>
26
- cds.requires.uaa && cds.requires.uaa.credentials && cds.requires.uaa.credentials.identityzoneid === tenant
27
+ cds.requires.auth && cds.requires.auth.credentials && cds.requires.auth.credentials.identityzoneid === tenant
27
28
 
28
29
  const hasPersistentOutbox = (srv, tenant) => {
29
30
  if (!cds.requires.outbox || cds.requires.outbox.kind !== 'persistent-outbox') return false
@@ -86,16 +87,15 @@ const processMessages = async (service, tenant, _opts = {}) => {
86
87
  } catch (e) {
87
88
  // could potentially be a timeout
88
89
  const _waitingTime = waitingTime(opts.attempt)
89
- LOG._error &&
90
- LOG.error(
91
- 'Outbox SELECT FOR UPDATE failed',
92
- opts.attempt > 0
93
- ? ''
94
- : {
95
- cause: e
96
- },
97
- `Retrying in ${Math.round(_waitingTime / 1000)} s`
98
- )
90
+ LOG.error(
91
+ 'Outbox SELECT FOR UPDATE failed',
92
+ opts.attempt > 0
93
+ ? ''
94
+ : {
95
+ cause: e
96
+ },
97
+ `Retrying in ${Math.round(_waitingTime / 1000)} s`
98
+ )
99
99
  outboxRunner.schedule(
100
100
  {
101
101
  name,
@@ -127,7 +127,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
127
127
  const _waitingTime = waitingTime(error.failedMessage.attempts)
128
128
  const info = { service: name, event: error.failedMessage.event }
129
129
  if (error.failedMessage.attempts > 0) info.cause = error.failedMessage.error
130
- LOG._error && LOG.error('Emit failed', info, `Retrying in ${Math.round(_waitingTime / 1000)} s`)
130
+ LOG.error('Emit failed', info, `Retrying in ${Math.round(_waitingTime / 1000)} s`)
131
131
  await UPDATE(messagesEntity)
132
132
  .where({ ID: error.failedMessage.ID })
133
133
  .set({ attempts: { '+=': 1 } })
@@ -140,12 +140,11 @@ const processMessages = async (service, tenant, _opts = {}) => {
140
140
  () => processMessages(service, tenant, opts)
141
141
  )
142
142
  } else {
143
- LOG._error &&
144
- LOG.error(
145
- 'Emit failed',
146
- { service: name, event: error.failedMessage.event, cause: error.failedMessage.error },
147
- 'Unrecoverable, outbox entry deleted'
148
- )
143
+ LOG.error(
144
+ 'Emit failed',
145
+ { service: name, event: error.failedMessage.event, cause: error.failedMessage.error },
146
+ 'Unrecoverable, outbox entry deleted'
147
+ )
149
148
  }
150
149
  } else {
151
150
  outboxRunner.success({ name, tenant })
@@ -0,0 +1,91 @@
1
+ /* eslint-disable prettier/prettier */
2
+ const redis = require('redis')
3
+ const cds = require('../../../lib')
4
+ const waitingTime = require('./common-utils/waitingTime')
5
+ const normalizeIncomingMessage = require('./common-utils/normalizeIncomingMessage')
6
+ const { hasPersistentOutbox } = require('./outbox/utils')
7
+
8
+ const _handleReconnects = (client, LOG) => {
9
+ client.on('reconnecting', () => {
10
+ LOG.warn('Reconnecting')
11
+ })
12
+
13
+ client.on('error', error => {
14
+ LOG.warn('Failed to connect to Redis: ', error)
15
+ })
16
+ }
17
+
18
+ class RedisMessaging extends cds.MessagingService {
19
+ async init() {
20
+ await super.init()
21
+ const credentials = this.options && this.options.credentials
22
+ const config = {
23
+ socket: {
24
+ reconnectStrategy: attempts => {
25
+ const _waitingTime = waitingTime(attempts)
26
+ this.LOG.warn(`Connection to Redis lost: Reconnecting in ${Math.round(_waitingTime / 1000)} s`)
27
+ return _waitingTime
28
+ }
29
+ }
30
+ }
31
+ // rediss://someUserName:somePassword@hostName:port -> rediss://:somePassword@hostName:port
32
+ const url = credentials && credentials.uri && credentials.uri.replace(/\/\/.*?:/, '//:')
33
+ if (!url) this.LOG._warn && this.LOG.warn('No Redis credentials found, using default credentials')
34
+ else config.url = url
35
+ this.client = redis.createClient(config)
36
+ _handleReconnects(this.client, this.LOG)
37
+
38
+ try {
39
+ await this.client.connect()
40
+ } catch (e) {
41
+ throw new Error('Connection to Redis could not be established: ' + e)
42
+ }
43
+
44
+ this._ready = true
45
+ this.client.on('end', () => {
46
+ this._ready = false
47
+ })
48
+ this.client.on('error', () => {
49
+ this._ready = false
50
+ })
51
+ this.client.on('ready', () => {
52
+ this._ready = true
53
+ })
54
+
55
+ cds.once('listening', () => {
56
+ this.startListening()
57
+ })
58
+ }
59
+
60
+ async startListening() {
61
+ let subscriber
62
+ for (const topic of [...this.subscribedTopics].map(kv => kv[0])) {
63
+ if (!subscriber) {
64
+ // For subscriptions we need to duplicate the connection
65
+ subscriber = this.client.duplicate()
66
+ _handleReconnects(subscriber)
67
+ await subscriber.connect()
68
+ }
69
+ this.LOG._info && this.LOG('Create subscription', { topic })
70
+ await subscriber.subscribe(topic, async message => {
71
+ const msg = normalizeIncomingMessage(message)
72
+ msg.event = topic
73
+ try {
74
+ await super.emit(msg)
75
+ } catch (e) {
76
+ this.LOG.error('ERROR occured in asynchronous event processing:', e)
77
+ }
78
+ })
79
+ }
80
+ }
81
+
82
+ async emit(msg) {
83
+ const _msg = this.message4(msg)
84
+ this.LOG._info && this.LOG.info('Emit', { topic: _msg.event })
85
+ if (!this._ready && hasPersistentOutbox(this, cds.context && cds.context.tenant))
86
+ throw new Error('Redis connection not ready')
87
+ await this.client.publish(_msg.event, JSON.stringify({ data: _msg.data, ...(_msg.headers || {}) }))
88
+ }
89
+ }
90
+
91
+ module.exports = RedisMessaging
@@ -1,12 +1,11 @@
1
1
  const cds = require('../cds')
2
- const LOG = cds.log('messaging')
3
2
  const queued = require('./common-utils/queued')
4
3
  const OutboxService = require('./Outbox')
5
4
 
6
5
  const _topic = declared => declared['@topic'] || declared.name
7
6
 
8
7
  let usedTopicOnce = false
9
- const _warnAndStripTopicPrefix = event => {
8
+ const _warnAndStripTopicPrefix = (event, LOG) => {
10
9
  if (event.startsWith('topic:')) {
11
10
  // backwards compatibility
12
11
  event = event.replace(/topic:/, '')
@@ -24,6 +23,7 @@ class MessagingService extends OutboxService {
24
23
  // enables queued async operations (without awaiting)
25
24
  this.queued = queued()
26
25
  this.subscribedTopics = new Map()
26
+ this.LOG = cds.log(this.kind ? `${this.kind}|messaging` : 'messaging')
27
27
  // Only for one central `messaging` service, otherwise all technical services would register themselves
28
28
  if (this.name === 'messaging') {
29
29
  this._registeredServices = new Map()
@@ -81,7 +81,7 @@ class MessagingService extends OutboxService {
81
81
  }
82
82
 
83
83
  on(event, cb) {
84
- const _event = _warnAndStripTopicPrefix(event)
84
+ const _event = _warnAndStripTopicPrefix(event, this.LOG)
85
85
  // save all subscribed topics (not needed for local-messaging)
86
86
  this.subscribedTopics.set(this.prepareTopic(_event, true), _event)
87
87
  return super.on(_event, cb)
@@ -109,9 +109,11 @@ class MessagingService extends OutboxService {
109
109
 
110
110
  message4(msg) {
111
111
  const _msg = { ...msg }
112
- if (msg.inbound && !cds.context)
113
- _msg.user = msg.tenant ? new cds.User.Privileged({ tenant: msg.tenant }) : new cds.User.Privileged()
114
- _msg.event = _warnAndStripTopicPrefix(_msg.event)
112
+ if (msg.inbound && !cds.context) {
113
+ // REVISIT: why are all inbound messages executed with privileged user?
114
+ cds.context = { tenant: msg.tenant, user: new cds.User.Privileged() }
115
+ }
116
+ _msg.event = _warnAndStripTopicPrefix(_msg.event, this.LOG)
115
117
  if (!_msg.headers) _msg.headers = {}
116
118
  if (!_msg.inbound) {
117
119
  _msg.headers = { ..._msg.headers } // don't change the original object
@@ -40,12 +40,13 @@ const _setCorrectValue = (el, data, params, kind) => {
40
40
  const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
41
41
  const funcParams = []
42
42
  const queryOptions = []
43
- for (const el in data) {
43
+ // REVISIT: take params from params after importer fix (the keys should not be part of params)
44
+ for (const param in data) {
44
45
  if (kind === 'odata-v2') {
45
- funcParams.push(`${el}=${_setCorrectValue(el, data, params, kind)}`)
46
+ funcParams.push(`${param}=${_setCorrectValue(param, data, params, kind)}`)
46
47
  } else {
47
- funcParams.push(`${el}=@${el}`)
48
- queryOptions.push(`@${el}=${_setCorrectValue(el, data, params, kind)}`)
48
+ funcParams.push(`${param}=@${param}`)
49
+ queryOptions.push(`@${param}=${_setCorrectValue(param, data, params, kind)}`)
49
50
  }
50
51
  }
51
52
  return kind === 'odata-v2'
@@ -62,8 +63,17 @@ const _extractParamsFromData = (data, params) => {
62
63
 
63
64
  const _buildKeys = (req, kind) => {
64
65
  const keys = []
65
- for (const key in req.target.keys) {
66
- keys.push(`${key}=${formatVal(req.data[key], key, req.target, kind)}`)
66
+ if (req.params && req.params.length > 0) {
67
+ const p1 = req.params[0]
68
+ if (typeof p1 !== 'object') return [p1]
69
+ for (const key in req.target.keys) {
70
+ keys.push(`${key}=${formatVal(p1[key], key, req.target, kind)}`)
71
+ }
72
+ } else {
73
+ // REVISIT: shall we keep that or remove it?
74
+ for (const key in req.target.keys) {
75
+ keys.push(`${key}=${formatVal(req.data[key], key, req.target, kind)}`)
76
+ }
67
77
  }
68
78
  return keys
69
79
  }
@@ -83,7 +93,14 @@ const _handleBoundActionFunction = (srv, def, req, url) => {
83
93
 
84
94
  const _handleUnboundActionFunction = (srv, def, req, event) => {
85
95
  if (def.kind === 'action') {
86
- return srv.post(`/${event}`, req.data)
96
+ // REVISIT: only for "rest" unbound actions/functions, we enforce axios to return a buffer
97
+ // required by cds-mt
98
+ const isBinary =
99
+ srv.kind === 'rest' &&
100
+ def &&
101
+ def.returns &&
102
+ (def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
103
+ return srv.send({ method: 'POST', path: `/${event}`, data: req.data, _binary: isBinary })
87
104
  }
88
105
 
89
106
  const url =
@@ -97,11 +114,30 @@ const _handleV2ActionFunction = (srv, def, req, event, kind) => {
97
114
  return def.kind === 'function' ? srv.get(url) : srv.post(url, {})
98
115
  }
99
116
 
117
+ const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
118
+ const params = []
119
+ const data = req.data
120
+ // REVISIT: take params from def.params, after importer fix (the keys should not be part of params)
121
+ for (const param in req.data) {
122
+ params.push(`${param}=${formatVal(data[param], param, { elements: def.params }, kind)}`)
123
+ }
124
+ const keys = _buildKeys(req, this.kind)
125
+ if (keys.length === 1 && typeof req.params[0] !== 'object') {
126
+ params.push(`${Object.keys(req.target.keys)[0]}=${keys[0]}`)
127
+ } else {
128
+ params.push(...keys)
129
+ }
130
+ const url = `${`/${event}`}?${params.join('&')}`
131
+ return def.kind === 'function' ? srv.get(url) : srv.post(url, {})
132
+ }
133
+
100
134
  const _addHandlerActionFunction = (srv, def, target) => {
101
135
  const event = def.name.match(/\w*$/)[0]
102
136
  if (target) {
103
137
  srv.on(event, target, async function (req) {
104
138
  const shortEntityName = req.target.name.replace(`${this.namespace}.`, '')
139
+ if (this.kind === 'odata-v2')
140
+ return _handleV2BoundActionFunction(srv, def, req, `${shortEntityName}_${event}`, this.kind)
105
141
  const url = `/${shortEntityName}(${_buildKeys(req, this.kind).join(',')})/${this.namespace}.${event}`
106
142
  return _handleBoundActionFunction(srv, def, req, url)
107
143
  })
@@ -208,7 +244,7 @@ class RemoteService extends cds.Service {
208
244
 
209
245
  if (req.target && req.target.name && this.definition && req.target.name.startsWith(this.definition.name + '.')) {
210
246
  const result = await super.handle(req)
211
- // only post process if alias was explicitely set in query
247
+ // only post process if alias was explicitly set in query
212
248
  if (_selectOnlyWithAlias(req.query)) {
213
249
  return postProcess(req.query, result, this, true)
214
250
  }