@sap/cds 5.6.2 → 5.7.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 (183) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/_i18n/i18n_fr.properties +4 -4
  3. package/apis/cds.d.ts +7 -10
  4. package/apis/connect.d.ts +3 -3
  5. package/apis/core.d.ts +2 -4
  6. package/apis/models.d.ts +2 -3
  7. package/apis/ql.d.ts +0 -1
  8. package/apis/services.d.ts +7 -3
  9. package/bin/build/buildTaskFactory.js +16 -10
  10. package/bin/build/buildTaskProviderFactory.js +3 -3
  11. package/bin/build/constants.js +2 -1
  12. package/bin/build/provider/buildTaskProviderInternal.js +14 -14
  13. package/bin/build/provider/hana/2migration.js +2 -3
  14. package/bin/build/provider/hana/index.js +34 -0
  15. package/bin/build/provider/hana/migrationtable.js +90 -22
  16. package/bin/build/provider/hana/template/undeploy.json +5 -0
  17. package/bin/build/provider/node-cf/index.js +9 -2
  18. package/bin/serve.js +16 -18
  19. package/lib/compile/cdsc.js +15 -5
  20. package/lib/compile/etc/_localized.js +4 -4
  21. package/lib/compile/extend.js +8 -0
  22. package/lib/compile/index.js +3 -1
  23. package/lib/compile/minify.js +61 -0
  24. package/lib/compile/resolve.js +4 -1
  25. package/lib/compile/to/gql.js +9 -0
  26. package/lib/compile/to/sql.js +26 -30
  27. package/lib/connect/index.js +1 -1
  28. package/lib/core/entities.js +0 -3
  29. package/lib/core/infer.js +1 -0
  30. package/lib/core/reflect.js +0 -34
  31. package/lib/deploy.js +25 -17
  32. package/lib/env/defaults.js +3 -1
  33. package/lib/env/index.js +13 -4
  34. package/lib/env/presets.js +38 -0
  35. package/lib/env/requires.js +16 -11
  36. package/lib/index.js +13 -11
  37. package/lib/log/format/kibana.js +4 -2
  38. package/lib/log/index.js +2 -2
  39. package/lib/ql/Whereable.js +1 -0
  40. package/lib/req/cds-context.js +79 -0
  41. package/lib/req/context.js +5 -77
  42. package/lib/req/request.js +1 -1
  43. package/lib/serve/Service-api.js +8 -4
  44. package/lib/serve/Service-dispatch.js +0 -7
  45. package/lib/serve/Service-methods.js +6 -8
  46. package/lib/serve/Transaction.js +35 -30
  47. package/lib/serve/adapters.js +1 -4
  48. package/lib/utils/axios.js +1 -1
  49. package/libx/_runtime/audit/Service.js +44 -20
  50. package/libx/_runtime/audit/generic/personal/access.js +16 -11
  51. package/libx/_runtime/audit/generic/personal/modification.js +5 -5
  52. package/libx/_runtime/audit/generic/personal/utils.js +46 -37
  53. package/libx/_runtime/{common/auth → auth}/index.js +21 -7
  54. package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
  55. package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
  56. package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
  57. package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
  58. package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
  59. package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
  60. package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
  62. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
  63. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +26 -0
  73. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
  74. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  75. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  76. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  77. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
  78. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  79. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  80. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  81. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  84. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  85. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
  86. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  87. package/libx/_runtime/cds-services/services/Service.js +0 -6
  88. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  89. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
  90. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  91. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  92. package/libx/_runtime/cds-services/util/assert.js +1 -262
  93. package/libx/_runtime/cds.js +6 -9
  94. package/libx/_runtime/common/aspects/entity.js +1 -1
  95. package/libx/_runtime/common/composition/delete.js +4 -2
  96. package/libx/_runtime/common/composition/update.js +22 -35
  97. package/libx/_runtime/common/composition/utils.js +3 -7
  98. package/libx/_runtime/common/error/standardError.js +11 -0
  99. package/libx/_runtime/common/generic/auth.js +63 -33
  100. package/libx/_runtime/common/generic/crud.js +11 -23
  101. package/libx/_runtime/common/generic/input.js +20 -0
  102. package/libx/_runtime/common/generic/paging.js +2 -2
  103. package/libx/_runtime/common/generic/put.js +4 -10
  104. package/libx/_runtime/common/generic/sorting.js +12 -30
  105. package/libx/_runtime/common/perf/index.js +24 -0
  106. package/libx/_runtime/common/utils/cqn.js +58 -1
  107. package/libx/_runtime/common/utils/cqn2cqn4sql.js +297 -121
  108. package/libx/_runtime/common/utils/csn.js +38 -56
  109. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  110. package/libx/_runtime/common/utils/resolveView.js +4 -5
  111. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  112. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  113. package/libx/_runtime/common/utils/structured.js +35 -25
  114. package/libx/_runtime/db/Service.js +0 -6
  115. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  116. package/libx/_runtime/db/expand/expandCQNToJoin.js +38 -52
  117. package/libx/_runtime/db/expand/index.js +3 -1
  118. package/libx/_runtime/db/generic/arrayed.js +3 -1
  119. package/libx/_runtime/db/generic/input.js +52 -10
  120. package/libx/_runtime/db/generic/integrity.js +367 -26
  121. package/libx/_runtime/db/generic/virtual.js +51 -13
  122. package/libx/_runtime/db/query/update.js +9 -3
  123. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  124. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  125. package/libx/_runtime/fiori/generic/activate.js +1 -0
  126. package/libx/_runtime/fiori/generic/before.js +2 -1
  127. package/libx/_runtime/fiori/generic/edit.js +1 -0
  128. package/libx/_runtime/fiori/generic/patch.js +1 -1
  129. package/libx/_runtime/fiori/generic/read.js +155 -57
  130. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
  131. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  132. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
  133. package/libx/_runtime/fiori/utils/delete.js +7 -1
  134. package/libx/_runtime/hana/Service.js +1 -8
  135. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  136. package/libx/_runtime/hana/execute.js +10 -4
  137. package/libx/_runtime/hana/pool.js +55 -45
  138. package/libx/_runtime/hana/search.js +7 -6
  139. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  140. package/libx/_runtime/hana/searchToContains.js +3 -1
  141. package/libx/_runtime/index.js +5 -5
  142. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  143. package/libx/_runtime/messaging/Outbox.js +53 -0
  144. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  145. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  146. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  147. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  148. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  149. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  150. package/libx/_runtime/messaging/file-based.js +5 -5
  151. package/libx/_runtime/messaging/message-queuing.js +2 -3
  152. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  153. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  154. package/libx/_runtime/messaging/service.js +16 -30
  155. package/libx/_runtime/remote/Service.js +15 -0
  156. package/libx/_runtime/remote/utils/client.js +15 -3
  157. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  158. package/libx/_runtime/sqlite/Service.js +7 -10
  159. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  160. package/libx/_runtime/sqlite/execute.js +18 -12
  161. package/libx/_runtime/types/api.js +2 -1
  162. package/libx/gql/resolvers/parse/ast2cqn/columns.js +1 -1
  163. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
  164. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  165. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +182 -118
  166. package/libx/odata/index.js +18 -15
  167. package/libx/odata/parser.js +1 -0
  168. package/libx/odata/utils.js +57 -0
  169. package/libx/rest/RestAdapter.js +2 -6
  170. package/libx/rest/utils/data.js +1 -6
  171. package/package.json +4 -3
  172. package/server.js +4 -5
  173. package/srv/audit-log.cds +87 -0
  174. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  175. package/srv/flex.js +1 -0
  176. package/srv/outbox.cds +11 -0
  177. package/srv/outbox.js +0 -0
  178. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  179. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  180. package/libx/odata/odata2cqn/index.js +0 -3
  181. package/libx/odata/odata2cqn/parser.js +0 -1
  182. package/libx/odata/readme.md +0 -1
  183. package/libx/odata/utils/index.js +0 -64
@@ -6,10 +6,9 @@ const hana = require('./driver')
6
6
 
7
7
  const _require = require('../common/utils/require')
8
8
 
9
- let im
9
+ function multiTenantInstanceManager(config = cds.env.requires.db) {
10
+ const { credentials } = config
10
11
 
11
- function multiTenantInstanceManager(db = cds.env.requires.db) {
12
- const credentials = db.credentials
13
12
  if (
14
13
  !credentials ||
15
14
  typeof credentials !== 'object' ||
@@ -22,10 +21,23 @@ function multiTenantInstanceManager(db = cds.env.requires.db) {
22
21
  return new Promise((resolve, reject) => {
23
22
  // REVISIT: better cache settings? current copied from old cds-hana...
24
23
  // note: may need to be low for mtx tests -> configurable?
25
- const opts = Object.assign(credentials, {
24
+ const opts = {
26
25
  cache_max_items: 1,
27
26
  cache_item_expire_seconds: 1
28
- })
27
+ }
28
+
29
+ // check binding to both managed-hana and service-manager for instance migration
30
+ if (cds.env.features.hybrid_instance_manager && process.env.VCAP_SERVICES) {
31
+ const vcap = JSON.parse(process.env.VCAP_SERVICES)
32
+ if (vcap['managed-hana'] && vcap['service-manager']) {
33
+ opts.smOpts = vcap['service-manager'][0].credentials
34
+ opts.imOpts = vcap['managed-hana'][0].credentials
35
+ }
36
+ }
37
+
38
+ // no double config -> take passed credentials (= cds.env.requires.db.credentials)
39
+ if (!opts.smOpts) Object.assign(opts, credentials)
40
+
29
41
  // REVISIT: should be relative
30
42
  // const mtxPath = require.resolve('@sap/cds-mtx', { paths: [process.env.pwd(), __dirname] })
31
43
  // const imPath = require.resolve('@sap/instance-manager', { paths: [mtxPath] })
@@ -37,29 +49,29 @@ function multiTenantInstanceManager(db = cds.env.requires.db) {
37
49
  })
38
50
  }
39
51
 
40
- function singleTenantInstanceManager(db = cds.env.requires.db) {
41
- const credentials = db.credentials
52
+ function singleTenantInstanceManager(config = cds.env.requires.db) {
53
+ const { credentials } = config
42
54
 
43
55
  if (!credentials || typeof credentials !== 'object' || !credentials.host) {
44
- throw Object.assign(new Error('No or malformed db credentials'), { credentials: credentials })
56
+ throw Object.assign(new Error('No or malformed db credentials'), { credentials })
45
57
  }
46
58
 
47
59
  // mock instance manager
48
60
  return {
49
- get: (_, cb) => {
50
- cb(null, { credentials: credentials })
51
- }
61
+ get: (_, cb) => cb(null, { credentials })
52
62
  }
53
63
  }
54
64
 
55
- async function credentials4(tenant, credentials) {
56
- if (!im) {
57
- const opts = credentials ? { credentials } : undefined
58
- im = cds.env.requires.db.multiTenant ? await multiTenantInstanceManager(opts) : singleTenantInstanceManager(opts)
65
+ async function credentials4(tenant, db) {
66
+ if (!db._instance_manager) {
67
+ const opts = db.options && db.options.credentials ? db.options : undefined
68
+ db._instance_manager = cds.env.requires.db.multiTenant
69
+ ? await multiTenantInstanceManager(opts)
70
+ : singleTenantInstanceManager(opts)
59
71
  }
60
72
 
61
73
  return new Promise((resolve, reject) => {
62
- im.get(tenant, (err, res) => {
74
+ db._instance_manager.get(tenant, (err, res) => {
63
75
  if (err) return reject(err)
64
76
  if (!res)
65
77
  return reject(Object.assign(new Error(`There is no instance for tenant "${tenant}"`), { statusCode: 404 }))
@@ -85,29 +97,12 @@ function factory4(creds, tenant) {
85
97
  /*
86
98
  * default generic-pool config
87
99
  */
88
- const config = { min: 0, max: 100, testOnBorrow: true }
89
-
90
- // REVISIT: copied from old cds-hana
91
- const _getMassagedCreds = function (creds) {
92
- if (!('ca' in creds) && creds.certificate) {
93
- creds.ca = creds.certificate
94
- }
95
- if ('encrypt' in creds && !('useTLS' in creds)) {
96
- creds.useTLS = creds.encrypt
97
- }
98
- if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds)) {
99
- creds.sslHostNameInCertificate = creds.hostname_in_certificate
100
- }
101
- if ('validate_certificate' in creds && !('sslValidateCertificate' in creds)) {
102
- creds.sslValidateCertificate = creds.validate_certificate
103
- }
104
- return creds
105
- }
100
+ const defaultConfig = { min: 0, max: 100, testOnBorrow: true }
106
101
 
107
102
  const _getPoolConfig = function () {
108
103
  const { pool: poolConfig } = cds.env.requires.db
109
104
 
110
- const mergedConfig = Object.assign({}, config, poolConfig)
105
+ const mergedConfig = Object.assign({}, defaultConfig, poolConfig)
111
106
 
112
107
  // defaults
113
108
  if (!poolConfig) {
@@ -134,14 +129,31 @@ const _getPoolConfig = function () {
134
129
  return mergedConfig
135
130
  }
136
131
 
132
+ // REVISIT: copied from old cds-hana
133
+ const _getMassagedCreds = function (creds) {
134
+ if (!('ca' in creds) && creds.certificate) {
135
+ creds.ca = creds.certificate
136
+ }
137
+ if ('encrypt' in creds && !('useTLS' in creds)) {
138
+ creds.useTLS = creds.encrypt
139
+ }
140
+ if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds)) {
141
+ creds.sslHostNameInCertificate = creds.hostname_in_certificate
142
+ }
143
+ if ('validate_certificate' in creds && !('sslValidateCertificate' in creds)) {
144
+ creds.sslValidateCertificate = creds.validate_certificate
145
+ }
146
+ return creds
147
+ }
148
+
137
149
  const pools = new Map()
138
150
 
139
- async function pool4(tenant, credentials) {
151
+ async function pool4(tenant, db) {
140
152
  if (!pools.get(tenant)) {
141
153
  pools.set(
142
154
  tenant,
143
155
  new Promise((resolve, reject) => {
144
- credentials4(tenant, credentials)
156
+ credentials4(tenant, db)
145
157
  .then(creds => {
146
158
  const config = _getPoolConfig()
147
159
  LOG._info && LOG.info('effective pool configuration:', config)
@@ -217,8 +229,8 @@ async function resilientAcquire(pool, attempts = 1) {
217
229
  }
218
230
 
219
231
  module.exports = {
220
- acquire: async (tenant, credentials) => {
221
- const pool = await pool4(tenant, credentials)
232
+ acquire: async (tenant, db) => {
233
+ const pool = await pool4(tenant, db)
222
234
  const _attempts = cds.env.requires.db.connection_attempts
223
235
  const attempts = _attempts && !isNaN(_attempts) && parseInt(_attempts)
224
236
  const client = await resilientAcquire(pool, attempts)
@@ -229,12 +241,10 @@ module.exports = {
229
241
  return client._pool.release(client)
230
242
  },
231
243
  drain: async tenant => {
232
- if (!pools.get(tenant)) {
233
- return
234
- }
235
- const p = await pool4(tenant)
244
+ const pool = pools.get(tenant)
245
+ if (!pool) return
236
246
  pools.delete(tenant)
237
- await p.drain()
238
- await p.clear()
247
+ await pool.drain()
248
+ await pool.clear()
239
249
  }
240
250
  }
@@ -1,18 +1,19 @@
1
1
  const cds = require('../cds')
2
2
  const search2cqn4sql = require('./search2cqn4sql')
3
3
 
4
+ const _setSearchOptions = (query, _searchOptions) => {
5
+ if (!(query && query.SELECT)) return
6
+ if (query.SELECT.search) Object.defineProperty(query, '_searchOptions', { value: _searchOptions })
7
+ if (query.SELECT.from.SELECT) _setSearchOptions(query.SELECT.from, _searchOptions)
8
+ }
9
+
4
10
  function searchHandler(req) {
5
11
  // REVISIT: remove feature toggle optimized_search after grace period
6
12
  // inject the search2cqn4sql module into the rewrite handler only when
7
13
  // the optimized search feature toggle is turned on
8
14
  if (!cds.env.features.optimized_search) return
9
15
 
10
- const query = req.query
11
- const search = query && query.SELECT && query.SELECT.search
12
-
13
- if (search) {
14
- Object.defineProperty(req.query, '_searchOptions', { value: { search2cqn4sql, locale: req.locale } })
15
- }
16
+ _setSearchOptions(req.query, { search2cqn4sql, locale: req.locale })
16
17
  }
17
18
 
18
19
  // handlers marked with `._initial = true` run in sequence
@@ -28,12 +28,13 @@ const search2cqn4sql = (query, entity, options) => {
28
28
 
29
29
  // If the localized association is defined for the target entity,
30
30
  // there should be at least one localized element.
31
- const resolveLocalizedTextsAtRuntime = !!localizedAssociation
31
+ const resolveLocalizedDataAtRuntime = !!localizedAssociation
32
32
 
33
33
  // suppress the localize handler from redirecting the query's target to the localized view
34
34
  Object.defineProperty(query, '_suppressLocalization', { value: true })
35
35
 
36
- if (resolveLocalizedTextsAtRuntime) {
36
+ // do not join if subquery exists - already done in there
37
+ if (resolveLocalizedDataAtRuntime && !query.SELECT.from.SELECT) {
37
38
  const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
38
39
 
39
40
  // replace $user_locale placeholder with the user locale or the HANA session context
@@ -52,15 +53,17 @@ const search2cqn4sql = (query, entity, options) => {
52
53
  let expression
53
54
 
54
55
  if (useContains) {
55
- expression = searchToContains(cqnSearchPhrase, columnsToBeSearched)
56
+ const funcCols = columnsToBeSearched.filter(col => col.func)
57
+ const refCols = columnsToBeSearched.filter(col => !col.func)
58
+ expression = [searchToContains(cqnSearchPhrase, refCols)]
59
+ if (funcCols.length) expression.push('or', ...searchToLike(cqnSearchPhrase, funcCols))
56
60
  } else {
57
61
  // No CONTAINS optimization possible. The search implementation for localized
58
62
  // texts falls back to the LIKE predicate.
59
63
  expression = searchToLike(cqnSearchPhrase, columnsToBeSearched)
60
64
  }
61
-
62
65
  // REVISIT: find out here if where or having must be used
63
- query._aggregated ? query.having(expression) : query.where(expression)
66
+ query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
64
67
  return query
65
68
  }
66
69
 
@@ -67,9 +67,11 @@ const searchToContains = (cqnSearchPhrase, columns) => {
67
67
  const isContainsPredicateSupported = query => {
68
68
  const cqnSearchPhrase = query.SELECT.search
69
69
 
70
+ if (cqnSearchPhrase && cqnSearchPhrase[0] && cqnSearchPhrase[0].val === ' ') return false
71
+
70
72
  // REVISIT: In the future, to further optimize search queries, you might
71
73
  // want to remove the following condition(s).
72
- if (query._aggregated) return false
74
+ if (query._aggregated || /* new parser */ query.SELECT.groupBy) return false
73
75
 
74
76
  // REVISIT: search terms starting with whitespace after a `NOT` operator does not
75
77
  // return the expected result on SAP HANA (BCP 2180256508). In addition, double
@@ -14,13 +14,13 @@ module.exports = {
14
14
  }
15
15
  },
16
16
 
17
- /** @type {import('./common/auth')} */
17
+ /** @type {import('./auth')} */
18
18
  get auth() {
19
- return this._auth || (this._auth = require('./common/auth'))
19
+ return this._auth || (this._auth = require('./auth'))
20
20
  },
21
21
 
22
- // REVISIT: remove once not needed anymore
23
- get performanceMeasurement() {
24
- return this._perf || (this._perf = require('./cds-services/adapter/perf/performanceMeasurement'))
22
+ /** @type {import('./common/perf')} */
23
+ get perf() {
24
+ return this._perf || (this._perf = require('./common/perf'))
25
25
  }
26
26
  }
@@ -21,11 +21,11 @@ class AMQPWebhookMessaging extends MessagingService {
21
21
  return super.init()
22
22
  }
23
23
 
24
- async emit(event, ...etc) {
25
- const msg = this.message4(event, ...etc)
24
+ async emit(msg) {
25
+ const _msg = this.message4(msg)
26
26
  const client = this.getClient()
27
27
  await this.queued(() => {})()
28
- return client.emit(msg)
28
+ return client.emit(_msg)
29
29
  }
30
30
 
31
31
  startListening(opt = {}) {
@@ -0,0 +1,53 @@
1
+ const cds = require('../cds')
2
+
3
+ const {
4
+ processMessages,
5
+ registerMessageProcessor,
6
+ writeInOutbox,
7
+ hasPersistentOutbox,
8
+ isUnrecoverable
9
+ } = require('./outbox/utils')
10
+
11
+ class OutboxService extends cds.Service {
12
+ // eslint-disable-next-line require-await
13
+ async init() {
14
+ // REVISIT: add 'outbox' to list of module names?
15
+ const LOG = cds.log(this.name)
16
+
17
+ // REVISIT: Also allow to overwrite this.send
18
+ this._emitImmediate = this.emit
19
+ this.emit = async function (...args) {
20
+ const msg = typeof args[0] === 'object' ? args[0] : { event: args[0], data: args[1], headers: args[2] }
21
+ const context = this.context || cds.context
22
+ if (this.options.outbox && context && typeof context.on === 'function') {
23
+ const outboxOpts = Object.assign(
24
+ {},
25
+ (typeof cds.requires.outbox === 'object' && cds.requires.outbox) || {},
26
+ (this.options && typeof this.options.outbox === 'object' && this.options.outbox) || {}
27
+ )
28
+ if (hasPersistentOutbox(this, context.tenant)) {
29
+ // returns true if not yet registered
30
+ if (registerMessageProcessor(this.name, context)) {
31
+ context.on('succeeded', () => processMessages(this, context.tenant, outboxOpts))
32
+ }
33
+ await writeInOutbox(this.name, msg, context)
34
+ return
35
+ }
36
+ // Revisit: Also allow maxAttempts?
37
+ context.on('succeeded', async () => {
38
+ try {
39
+ await this._emitImmediate(msg)
40
+ } catch (e) {
41
+ LOG._error && LOG.error('Emit failed', { event: msg.event, cause: e })
42
+ // opts.crashOnError is not official!!!
43
+ if (isUnrecoverable(this, e) && outboxOpts.crashOnError !== false) process.exit(1)
44
+ }
45
+ })
46
+ return
47
+ }
48
+ return this._emitImmediate(msg)
49
+ }
50
+ }
51
+ }
52
+
53
+ module.exports = OutboxService
@@ -2,6 +2,7 @@ const cds = require('../../cds.js')
2
2
  const LOG = cds.log('messaging')
3
3
  const ClientAmqp = require('@sap/xb-msg-amqp-v100').Client
4
4
  const { connect, disconnect } = require('./connections')
5
+ const { hasPersistentOutbox } = require('../outbox/utils')
5
6
 
6
7
  const _JSONorString = string => {
7
8
  try {
@@ -33,16 +34,19 @@ const addDataListener = (client, queue, prefix, cb) =>
33
34
  })
34
35
  })
35
36
 
36
- const sender = (client, optionsApp) => client.sender(`${optionsApp.appName}-${optionsApp.appID}`).attach('')
37
+ const sender = (client, optionsApp) => client.sender(`${optionsApp.appName}-${optionsApp.appID}`)
37
38
 
38
- const emit = ({ data, event: topic, headers = {} }, sender, prefix) =>
39
+ const emit = ({ data, event: topic, headers = {} }, stream, prefix) =>
39
40
  new Promise((resolve, reject) => {
40
41
  LOG._info && LOG.info('Emit', { topic })
41
42
  const message = { ...headers, data }
42
43
  const payload = { chunks: [Buffer.from(JSON.stringify(message))], type: 'application/json' }
43
44
  const msg = {
44
45
  done: resolve,
45
- failed: reject,
46
+ failed: e => {
47
+ if (e.condition === 'amqp:not-allowed') e.unrecoverable = true
48
+ reject(e)
49
+ },
46
50
  payload,
47
51
  target: {
48
52
  properties: {
@@ -50,21 +54,21 @@ const emit = ({ data, event: topic, headers = {} }, sender, prefix) =>
50
54
  }
51
55
  }
52
56
  }
53
- sender.write(msg)
57
+ stream.write(msg)
54
58
  })
55
59
 
56
60
  class AMQPClient {
57
- constructor({ optionsAMQP, optionsApp, queueName, prefix, keepAlive = true }) {
61
+ constructor({ optionsAMQP, prefix, service, keepAlive = true }) {
58
62
  this.optionsAMQP = optionsAMQP
59
- this.optionsApp = optionsApp
60
- this.queueName = queueName
61
63
  this.prefix = prefix
62
64
  this.keepAlive = keepAlive
65
+ this.service = service
63
66
  }
64
67
 
65
68
  connect() {
66
69
  this.client = new ClientAmqp(this.optionsAMQP)
67
- this.sender = sender(this.client, this.optionsApp)
70
+ this.sender = sender(this.client, this.service.optionsApp)
71
+ this.stream = this.sender.attach('')
68
72
  return connect(this.client, this.keepAlive)
69
73
  }
70
74
 
@@ -77,12 +81,15 @@ class AMQPClient {
77
81
 
78
82
  async emit(msg) {
79
83
  if (!this.client) await this.connect()
80
- await emit(msg, this.sender, this.prefix.topic)
84
+ // REVISIT: Is this a robust way to find out if the connection is working?
85
+ if (hasPersistentOutbox(this.service, cds.context && cds.context.tenant) && !this.sender.opened())
86
+ throw new Error('AMQP: Sender is not open')
87
+ await emit(msg, this.stream, this.prefix.topic)
81
88
  if (!this.keepAlive) return this.disconnect()
82
89
  }
83
90
 
84
91
  listen(cb) {
85
- return addDataListener(this.client, this.queueName, this.prefix.queue, cb)
92
+ return addDataListener(this.client, this.service.queueName, this.prefix.queue, cb)
86
93
  }
87
94
  }
88
95
 
@@ -1,17 +1,22 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log('messaging')
3
-
4
- const MAX_WAITING_TIME = 1480000
5
-
6
- const _waitingTime = x => (x > 18 ? MAX_WAITING_TIME : (Math.pow(1.5, x) + Math.random()) * 1000)
3
+ const waitingTime = require('./waitingTime')
7
4
 
8
5
  const _connectUntilConnected = (client, x) => {
6
+ const _waitingTime = waitingTime(x)
9
7
  setTimeout(() => {
10
- connect(client, true).catch(e => {
11
- LOG._warn && LOG.warn(`Connection to Enterprise Messaging Client lost: Unsuccessful attempt to reconnect (${x}).`)
12
- _connectUntilConnected(client, x + 1)
13
- })
14
- }, _waitingTime(x))
8
+ connect(client, true)
9
+ .then(() => {
10
+ LOG._warn && LOG.warn('Reconnected to Enterprise Messaging Client')
11
+ })
12
+ .catch(e => {
13
+ LOG._warn &&
14
+ LOG.warn(
15
+ `Connection to Enterprise Messaging Client lost: Reconnecting in ${Math.round(_waitingTime / 1000)} s`
16
+ )
17
+ _connectUntilConnected(client, x + 1)
18
+ })
19
+ }, _waitingTime)
15
20
  }
16
21
 
17
22
  const connect = (client, keepAlive) => {
@@ -0,0 +1,2 @@
1
+ const MAX_WAITING_TIME = 1480000
2
+ module.exports = x => (x > 18 ? MAX_WAITING_TIME : (Math.pow(1.5, x) + Math.random()) * 1000)
@@ -17,9 +17,8 @@ class EnterpriseMessagingShared extends AMQPWebhookMessaging {
17
17
  const optionsAMQP = optionsMessaging(this.options, 'amqp10ws')
18
18
  this.client = new AMQPClient({
19
19
  optionsAMQP,
20
- optionsApp: this.optionsApp,
21
- queueName: this.queueName,
22
- prefix: { topic: 'topic:', queue: 'queue:' }
20
+ prefix: { topic: 'topic:', queue: 'queue:' },
21
+ service: this
23
22
  })
24
23
  return this.client
25
24
  }
@@ -3,7 +3,7 @@ const LOG = cds.log('messaging')
3
3
  const express = require('express')
4
4
  const getTenantInfo = require('./getTenantInfo.js')
5
5
  const isSecured = () => cds.requires.uaa && cds.requires.uaa.credentials
6
- const { getTenant } = require('../../common/auth/strategies/utils/xssec.js')
6
+ const { getTenant } = require('../../auth/strategies/utils/xssec.js')
7
7
 
8
8
  const _isAll = a => a && a.includes('all')
9
9
  const _hasScope = (scope, req) =>
@@ -16,7 +16,7 @@ class EndpointRegistry {
16
16
  this.webhookCallbacks = new Map()
17
17
  this.deployCallbacks = new Map()
18
18
  if (isSecured()) {
19
- const JWTStrategy = require('../../common/auth/strategies/JWT.js')
19
+ const JWTStrategy = require('../../auth/strategies/JWT.js')
20
20
  const passport = require('passport')
21
21
  passport.use(new JWTStrategy(cds.requires.uaa))
22
22
  paths.forEach(path => {
@@ -162,21 +162,27 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
162
162
 
163
163
  await this.queued(() => {})()
164
164
 
165
- return authorizedRequest({
166
- method: 'POST',
167
- uri: optionsMessagingREST.uri,
168
- path: `/messagingrest/v1/topics/${encodeURIComponent(topic)}/messages`,
169
- oa2: optionsMessagingREST.oa2,
170
- tenant,
171
- dataObj: message,
172
- headers: {
173
- 'x-qos': 1
174
- },
175
- attemptInfo: () => LOG._info && LOG.info('Emit', { topic }),
176
- errMsg,
177
- target: { kind: 'MESSAGE', topic },
178
- tokenStore: {}
179
- })
165
+ try {
166
+ await authorizedRequest({
167
+ method: 'POST',
168
+ uri: optionsMessagingREST.uri,
169
+ path: `/messagingrest/v1/topics/${encodeURIComponent(topic)}/messages`,
170
+ oa2: optionsMessagingREST.oa2,
171
+ tenant,
172
+ dataObj: message,
173
+ headers: {
174
+ 'x-qos': 1
175
+ },
176
+ attemptInfo: () => LOG._info && LOG.info('Emit', { topic }),
177
+ errMsg,
178
+ target: { kind: 'MESSAGE', topic },
179
+ tokenStore: {}
180
+ })
181
+ } catch (e) {
182
+ // Note: If the topic rules don't allow the topic, we get a 403 (which is a strange choice by Event Mesh)
183
+ if (e.response && (e.response.statusCode === 400 || e.response.statusCode === 403)) e.unrecoverable = true
184
+ throw e
185
+ }
180
186
  }
181
187
 
182
188
  wildcarded(topic) {
@@ -20,14 +20,14 @@ class FileBasedMessaging extends MessagingService {
20
20
  return super.init()
21
21
  }
22
22
 
23
- async emit(event, ...etc) {
24
- const msg = this.message4(event, ...etc)
25
- const e = msg.event
26
- delete msg.event
23
+ async emit(msg) {
24
+ const _msg = this.message4(msg)
25
+ const e = _msg.event
26
+ delete _msg.event
27
27
  await this.queued(lock)(this.file)
28
28
  LOG._debug && LOG.debug('Emit', { topic: e, file: this.file })
29
29
  try {
30
- await fs.appendFile(this.file, `\n${e} ${JSON.stringify(msg)}`)
30
+ await fs.appendFile(this.file, `\n${e} ${JSON.stringify(_msg)}`)
31
31
  } catch (e) {
32
32
  LOG._debug && LOG.debug('Error', e)
33
33
  } finally {
@@ -156,9 +156,8 @@ class MessageQueuing extends AMQPWebhookMessaging {
156
156
  const optionsAMQP = optionsMessaging(this.options)
157
157
  this.client = new AMQPClient({
158
158
  optionsAMQP,
159
- optionsApp: this.optionsApp,
160
- queueName: this.queueName,
161
- prefix: { topic: 'topic://', queue: 'queue://' }
159
+ prefix: { topic: 'topic://', queue: 'queue://' },
160
+ service: this
162
161
  })
163
162
  return this.client
164
163
  }
@@ -0,0 +1,75 @@
1
+ const PROCESSING = 'processing'
2
+ const LOCKED = 'locked'
3
+ const QUEUED = 'queued'
4
+ const SCHEDULED = 'scheduled'
5
+
6
+ class OutboxRunner {
7
+ constructor() {
8
+ this.states = new Map()
9
+ }
10
+
11
+ _setStateProp(prop, state, { name, tenant }) {
12
+ const statesSrv = this.states.get(name)
13
+ if (!statesSrv) {
14
+ const newStatesSrv = new Map()
15
+ newStatesSrv.set(tenant, { [prop]: state })
16
+ this.states.set(name, newStatesSrv)
17
+ return state
18
+ }
19
+ const obj = statesSrv.get(tenant)
20
+ if (!obj) {
21
+ statesSrv.set(tenant, { [prop]: state })
22
+ return state
23
+ }
24
+ obj[prop] = state
25
+ return state
26
+ }
27
+
28
+ _getStateProp(prop, { name, tenant }) {
29
+ const statesSrv = this.states.get(name)
30
+ if (!statesSrv) return
31
+ const obj = statesSrv.get(tenant)
32
+ return obj && obj[prop]
33
+ }
34
+
35
+ run({ name, tenant }, cb) {
36
+ const scheduled = this._getStateProp(SCHEDULED, { name, tenant })
37
+ if (scheduled) return // maybe make that configurable, we can also 'refresh' the current try
38
+ const processingState = this._getStateProp(PROCESSING, { name, tenant })
39
+ if (processingState === LOCKED) {
40
+ this._setStateProp(PROCESSING, QUEUED, { name, tenant })
41
+ return
42
+ }
43
+ if (processingState === QUEUED) return
44
+ if (!processingState) this._setStateProp(PROCESSING, LOCKED, { name, tenant })
45
+ return cb()
46
+ }
47
+
48
+ schedule({ name, tenant, waitingTime }, cb) {
49
+ if (this._getStateProp(SCHEDULED, { name, tenant })) return
50
+ const timer = setTimeout(() => {
51
+ this._setStateProp(SCHEDULED, undefined, { name, tenant })
52
+ return cb()
53
+ }, waitingTime)
54
+ this._setStateProp(SCHEDULED, timer, { name, tenant })
55
+ }
56
+
57
+ end({ name, tenant }, cb) {
58
+ const processingState = this._getStateProp(PROCESSING, { name, tenant })
59
+ this._setStateProp(PROCESSING, undefined, { name, tenant })
60
+ if (processingState === QUEUED) {
61
+ return cb()
62
+ }
63
+ }
64
+
65
+ success({ name, tenant }) {
66
+ const timer = this._getStateProp(SCHEDULED, { name, tenant })
67
+ if (timer) {
68
+ // once successful, we don't want to have another scheduled run
69
+ clearTimeout(timer)
70
+ this._setStateProp(SCHEDULED, undefined, { name, tenant })
71
+ }
72
+ }
73
+ }
74
+
75
+ module.exports = OutboxRunner