@sap/cds 6.1.2 → 6.2.1

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 (212) hide show
  1. package/CHANGELOG.md +92 -8
  2. package/apis/cds.d.ts +18 -6
  3. package/apis/connect.d.ts +1 -1
  4. package/apis/cqn.d.ts +1 -1
  5. package/apis/log.d.ts +23 -5
  6. package/apis/ql.d.ts +128 -61
  7. package/apis/services.d.ts +11 -0
  8. package/apis/test.d.ts +61 -0
  9. package/apis/utils.d.ts +15 -0
  10. package/app/fiori/preview.js +1 -0
  11. package/bin/build/buildTaskEngine.js +70 -22
  12. package/bin/build/buildTaskFactory.js +18 -11
  13. package/bin/build/buildTaskHandler.js +1 -1
  14. package/bin/build/buildTaskProviderFactory.js +3 -13
  15. package/bin/build/constants.js +0 -1
  16. package/bin/build/index.js +14 -6
  17. package/bin/build/provider/buildTaskHandlerEdmx.js +2 -3
  18. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -2
  19. package/bin/build/provider/buildTaskHandlerInternal.js +3 -6
  20. package/bin/build/provider/buildTaskProviderInternal.js +51 -39
  21. package/bin/build/provider/fiori/index.js +3 -3
  22. package/bin/build/provider/hana/2migration.js +1 -1
  23. package/bin/build/provider/hana/index.js +34 -27
  24. package/bin/build/provider/java/index.js +6 -7
  25. package/bin/build/provider/mtx/index.js +20 -18
  26. package/bin/build/provider/mtx/resourcesTarBuilder.js +8 -11
  27. package/bin/build/provider/mtx-sidecar/index.js +13 -17
  28. package/bin/build/provider/nodejs/index.js +8 -7
  29. package/bin/build/util.js +22 -4
  30. package/bin/cds.js +8 -4
  31. package/bin/deploy/to-hana/cfUtil.js +53 -18
  32. package/bin/mtx/in-cds.js +1 -0
  33. package/bin/serve.js +37 -30
  34. package/lib/auth/basic-auth.js +33 -0
  35. package/lib/auth/dummy-auth.js +7 -0
  36. package/lib/auth/ias-auth.js +2 -0
  37. package/lib/auth/index.js +31 -0
  38. package/lib/auth/jwt-auth.js +3 -0
  39. package/lib/auth/mocked-users.js +72 -0
  40. package/lib/auth/passport-basic.js +12 -0
  41. package/lib/auth/passport-digest.js +14 -0
  42. package/lib/auth/xsuaa-auth.js +3 -0
  43. package/lib/compile/cds-compile.js +3 -3
  44. package/lib/compile/to/cdl.js +5 -1
  45. package/lib/compile/to/edm.js +8 -0
  46. package/lib/compile/to/gql.js +1 -0
  47. package/lib/compile/to/json.js +30 -5
  48. package/lib/compile/to/sql.js +3 -1
  49. package/lib/core/index.js +5 -1
  50. package/lib/dbs/cds-deploy.js +36 -6
  51. package/lib/env/cds-env.js +15 -5
  52. package/lib/env/cds-requires.js +51 -58
  53. package/lib/env/defaults.js +1 -0
  54. package/lib/env/schemas/cds-package.json +4 -0
  55. package/lib/env/schemas/cds-rc.json +63 -77
  56. package/lib/i18n/localize.js +16 -5
  57. package/lib/index.js +9 -4
  58. package/lib/log/cds-error.js +4 -6
  59. package/lib/log/cds-log.js +89 -53
  60. package/lib/log/service/index.js +1 -0
  61. package/lib/ql/CREATE.js +2 -5
  62. package/lib/ql/DELETE.js +1 -1
  63. package/lib/ql/DROP.js +1 -3
  64. package/lib/ql/INSERT.js +3 -3
  65. package/lib/ql/Query.js +10 -23
  66. package/lib/ql/SELECT.js +1 -2
  67. package/lib/ql/UPDATE.js +2 -2
  68. package/lib/ql/Whereable.js +7 -15
  69. package/lib/ql/cds-ql.js +9 -3
  70. package/lib/req/cds-context.js +11 -3
  71. package/lib/req/context.js +29 -23
  72. package/lib/req/locale.js +9 -5
  73. package/lib/req/request.js +1 -0
  74. package/lib/req/user.js +2 -1
  75. package/lib/srv/cds-connect.js +1 -1
  76. package/lib/srv/cds-serve.js +21 -14
  77. package/lib/srv/middlewares/cds-context.js +29 -0
  78. package/lib/srv/middlewares/ctx-model.js +24 -0
  79. package/lib/srv/middlewares/errors.js +9 -0
  80. package/lib/srv/middlewares/index.js +22 -0
  81. package/lib/srv/middlewares/sap-statistics.js +13 -0
  82. package/lib/srv/middlewares/trace.js +102 -0
  83. package/lib/srv/protocols/_legacy.js +42 -0
  84. package/lib/srv/protocols/graphql.js +39 -0
  85. package/lib/srv/protocols/hcql.js +37 -0
  86. package/lib/srv/protocols/index.js +86 -0
  87. package/lib/srv/protocols/odata-v2-proxy.js +3767 -0
  88. package/lib/srv/protocols/odata-v2.js +26 -0
  89. package/lib/srv/protocols/odata-v4.js +16 -0
  90. package/lib/srv/protocols/rest.js +13 -0
  91. package/lib/srv/srv-api.js +5 -0
  92. package/lib/srv/srv-models.js +4 -6
  93. package/lib/utils/axios.js +3 -2
  94. package/lib/utils/cds-test.js +27 -21
  95. package/lib/utils/cds-utils.js +19 -20
  96. package/lib/utils/tar.js +175 -0
  97. package/libx/_runtime/audit/generic/personal/utils.js +18 -7
  98. package/libx/_runtime/audit/utils/v2.js +1 -0
  99. package/libx/_runtime/auth/index.js +4 -0
  100. package/libx/_runtime/auth/strategies/ias-auth.js +76 -0
  101. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +8 -3
  102. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +15 -4
  103. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  104. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +1 -1
  105. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
  106. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +9 -0
  107. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriInfo.js +5 -1
  108. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +12 -0
  109. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +6 -2
  110. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/RequestValidator.js +47 -7
  111. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  112. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -2
  113. package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -1
  114. package/libx/_runtime/cds-services/util/assert.js +7 -0
  115. package/libx/_runtime/common/aspects/relation.js +1 -1
  116. package/libx/_runtime/common/composition/data.js +61 -15
  117. package/libx/_runtime/common/composition/delete.js +0 -1
  118. package/libx/_runtime/common/composition/insert.js +0 -1
  119. package/libx/_runtime/common/composition/tree.js +4 -10
  120. package/libx/_runtime/common/composition/update.js +44 -21
  121. package/libx/_runtime/common/generic/auth/capabilities.js +8 -10
  122. package/libx/_runtime/common/generic/crud.js +1 -2
  123. package/libx/_runtime/common/generic/etag.js +4 -4
  124. package/libx/_runtime/common/generic/input.js +21 -6
  125. package/libx/_runtime/common/generic/paging.js +3 -3
  126. package/libx/_runtime/common/generic/put.js +7 -4
  127. package/libx/_runtime/common/generic/sorting.js +4 -4
  128. package/libx/_runtime/common/generic/temporal.js +3 -6
  129. package/libx/_runtime/common/i18n/messages.properties +0 -7
  130. package/libx/_runtime/common/utils/cqn2cqn4sql.js +11 -6
  131. package/libx/_runtime/common/utils/csn.js +0 -28
  132. package/libx/_runtime/common/utils/draft.js +8 -1
  133. package/libx/_runtime/common/utils/path.js +7 -1
  134. package/libx/_runtime/common/utils/propagateForeignKeys.js +122 -0
  135. package/libx/_runtime/common/utils/resolveView.js +2 -3
  136. package/libx/_runtime/common/utils/template.js +2 -3
  137. package/libx/_runtime/db/data-conversion/post-processing.js +3 -44
  138. package/libx/_runtime/db/generic/input.js +6 -6
  139. package/libx/_runtime/db/sql-builder/dataTypes.js +4 -0
  140. package/libx/_runtime/fiori/generic/activate.js +2 -2
  141. package/libx/_runtime/fiori/generic/before.js +40 -72
  142. package/libx/_runtime/fiori/generic/cancel.js +2 -2
  143. package/libx/_runtime/fiori/generic/delete.js +2 -2
  144. package/libx/_runtime/fiori/generic/edit.js +2 -2
  145. package/libx/_runtime/fiori/generic/new.js +3 -5
  146. package/libx/_runtime/fiori/generic/patch.js +49 -43
  147. package/libx/_runtime/fiori/generic/prepare.js +2 -2
  148. package/libx/_runtime/fiori/generic/read.js +27 -37
  149. package/libx/_runtime/fiori/utils/where.js +4 -2
  150. package/libx/_runtime/hana/Service.js +1 -3
  151. package/libx/_runtime/hana/conversion.js +3 -0
  152. package/libx/_runtime/hana/driver.js +33 -3
  153. package/libx/_runtime/hana/dynatrace.js +1 -0
  154. package/libx/_runtime/hana/search2Contains.js +12 -1
  155. package/libx/_runtime/hana/search2cqn4sql.js +10 -27
  156. package/libx/_runtime/hana/streaming.js +1 -0
  157. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  158. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -0
  159. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +5 -2
  160. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -0
  161. package/libx/_runtime/messaging/enterprise-messaging.js +62 -3
  162. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  163. package/libx/_runtime/messaging/redis-messaging.js +1 -0
  164. package/libx/_runtime/remote/Service.js +2 -2
  165. package/libx/_runtime/remote/utils/client.js +35 -11
  166. package/libx/_runtime/remote/utils/data.js +7 -2
  167. package/libx/_runtime/sqlite/Service.js +18 -7
  168. package/libx/_runtime/sqlite/conversion.js +3 -0
  169. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +3 -3
  170. package/libx/_runtime/sqlite/localized.js +8 -8
  171. package/libx/odata/afterburner.js +39 -7
  172. package/libx/odata/cqn2odata.js +6 -3
  173. package/libx/odata/grammar.pegjs +66 -18
  174. package/libx/odata/index.js +3 -2
  175. package/libx/odata/parser.js +1 -1
  176. package/libx/odata/utils.js +2 -0
  177. package/libx/rest/RestAdapter.js +62 -43
  178. package/libx/rest/middleware/input.js +2 -3
  179. package/libx/rest/middleware/parse.js +2 -1
  180. package/libx/rest/middleware/update.js +1 -1
  181. package/package.json +2 -2
  182. package/server.js +5 -4
  183. package/srv/mtx.cds +1 -1
  184. package/srv/mtx.js +4 -24
  185. package/lib/srv/adapters.js +0 -85
  186. package/lib/utils/resources/index.js +0 -48
  187. package/lib/utils/resources/tar.js +0 -49
  188. package/lib/utils/resources/utils.js +0 -11
  189. package/libx/_runtime/db/utils/propagateForeignKeys.js +0 -93
  190. package/libx/_runtime/extensibility/activate.js +0 -69
  191. package/libx/_runtime/extensibility/add.js +0 -50
  192. package/libx/_runtime/extensibility/addExtension.js +0 -72
  193. package/libx/_runtime/extensibility/defaults.js +0 -34
  194. package/libx/_runtime/extensibility/handler/transformREAD.js +0 -121
  195. package/libx/_runtime/extensibility/handler/transformRESULT.js +0 -51
  196. package/libx/_runtime/extensibility/handler/transformWRITE.js +0 -64
  197. package/libx/_runtime/extensibility/linter/allowlist_checker.js +0 -373
  198. package/libx/_runtime/extensibility/linter/annotations_checker.js +0 -113
  199. package/libx/_runtime/extensibility/linter/checker_base.js +0 -20
  200. package/libx/_runtime/extensibility/linter/namespace_checker.js +0 -180
  201. package/libx/_runtime/extensibility/linter.js +0 -32
  202. package/libx/_runtime/extensibility/push.js +0 -118
  203. package/libx/_runtime/extensibility/service.js +0 -38
  204. package/libx/_runtime/extensibility/token.js +0 -57
  205. package/libx/_runtime/extensibility/utils.js +0 -131
  206. package/libx/_runtime/extensibility/validation.js +0 -50
  207. package/libx/_runtime/extensibility/views.js +0 -12
  208. package/srv/extensibility-service.cds +0 -59
  209. package/srv/extensibility-service.js +0 -1
  210. package/srv/extensions.cds +0 -8
  211. package/srv/model-provider.cds +0 -61
  212. package/srv/model-provider.js +0 -143
@@ -129,9 +129,37 @@ function _connectHanaClient(creds, tenant) {
129
129
 
130
130
  let driver
131
131
 
132
- const _getHanaDriver = (name = 'hdb') => {
132
+ const _getHanaDriver = name => {
133
133
  if (driver) return driver
134
134
 
135
+ let isConfigured = false
136
+ if (!name) {
137
+ let packageJson
138
+ try {
139
+ packageJson = require(cds.root + '/package.json')
140
+ } catch (e) {
141
+ LOG._debug && LOG.debug(`Could not find package.json. Trying to lookup hana driver automatically.`)
142
+ name = 'hdb'
143
+ }
144
+
145
+ if (packageJson?.dependencies?.hdb) {
146
+ LOG._debug && LOG.debug(`"hdb" found in dependencies of "${cds.root}".`)
147
+ name = 'hdb'
148
+ isConfigured = true
149
+ } else if (packageJson?.dependencies?.['@sap/hana-client']) {
150
+ LOG._debug && LOG.debug(`"@sap/hana-client" found in dependencies of "${cds.root}".`)
151
+ name = '@sap/hana-client'
152
+ isConfigured = true
153
+ } else if (!name) {
154
+ LOG._debug &&
155
+ LOG.debug(
156
+ `Neither "hdb" nor "@sap/hana-client" found in dependencies of "${cds.root}". Trying to lookup hana driver automatically.`
157
+ )
158
+ // fallback to hdb in case both are not provided, which will fallback to @sap/hana-client if hdb is not installed
159
+ name = 'hdb'
160
+ }
161
+ }
162
+
135
163
  try {
136
164
  driver = Object.assign({ name }, require(name))
137
165
 
@@ -161,9 +189,11 @@ const _getHanaDriver = (name = 'hdb') => {
161
189
 
162
190
  return driver
163
191
  } catch (e) {
164
- if (name === 'hdb') {
192
+ if (name === 'hdb' && !isConfigured) {
165
193
  LOG._debug && LOG.debug(`Failed to require "hdb" with error "${e.message}". Trying "@sap/hana-client" next.`)
166
194
  return _getHanaDriver('@sap/hana-client')
195
+ } else if (isConfigured) {
196
+ throw new Error(`"${name}" could not be required. Please make sure it is installed.`)
167
197
  } else {
168
198
  throw new Error(
169
199
  'Neither "hdb" nor "@sap/hana-client" could be required. Please make sure one of them is installed.'
@@ -172,4 +202,4 @@ const _getHanaDriver = (name = 'hdb') => {
172
202
  }
173
203
  }
174
204
 
175
- module.exports = _getHanaDriver('hdb')
205
+ module.exports = _getHanaDriver()
@@ -1,5 +1,6 @@
1
1
  const dynatrace = {}
2
2
  try {
3
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
3
4
  dynatrace.sdk = require('@dynatrace/oneagent-sdk')
4
5
  dynatrace.api = dynatrace.sdk.createInstance()
5
6
  } catch (err) {
@@ -59,7 +59,7 @@ const search2Contains = (cqnSearchPhrase, columns) => {
59
59
  return expression
60
60
  }
61
61
 
62
- const isContainsPredicateSupported = query => {
62
+ const isContainsPredicateSupported = (query, entity, columns2Search) => {
63
63
  const cqnSearchPhrase = query.SELECT.search
64
64
 
65
65
  if (cqnSearchPhrase && cqnSearchPhrase[0] && cqnSearchPhrase[0].val === ' ') return false
@@ -84,9 +84,20 @@ const isContainsPredicateSupported = query => {
84
84
  // brackets are not supported as search operators in SAP HANA
85
85
  if (cqnSearchPhrase.some(searchXpr => searchXpr.xpr)) return false
86
86
 
87
+ // join not optimized
88
+ if (entity.query?.SELECT.from.join) return false
89
+
90
+ // the CONTAINS function does not interoperate with columns that use the CONCAT function
91
+ if (_isColumnFunc(columns2Search, entity.query?.SELECT.columns)) return false
87
92
  return true
88
93
  }
89
94
 
95
+ const _isColumnFunc = (columns2Search, columnsDefs) =>
96
+ columns2Search.some(column2Search => {
97
+ if (column2Search.func) return true
98
+ return columnsDefs?.some(columnDef => columnDef.func && columnDef.as === column2Search.ref[0])
99
+ })
100
+
90
101
  module.exports = {
91
102
  isContainsPredicateSupported,
92
103
  search2Contains
@@ -25,35 +25,29 @@ const search2cqn4sql = (query, entity, options) => {
25
25
  if (!cqnSearchPhrase) return query
26
26
 
27
27
  let { columns: columns2Search = computeColumnsToBeSearched(query, entity), locale } = options
28
- const localizedAssociation = _getLocalizedAssociation(entity)
28
+ const localizedAssociation = entity.associations?.localized
29
29
 
30
- // If the localized association is defined for the target entity,
31
- // there should be at least one localized element.
32
- const resolveLocalizedDataAtRuntime = !!localizedAssociation
33
-
34
- // suppress the localize handler from redirecting the query's target to the localized view
35
- Object.defineProperty(query, '_suppressLocalization', { value: true })
36
-
37
- const columnsDefs = entity.query && entity.query.SELECT.columns
38
- const isSomeColumn2SearchUseFunction = _isSomeColumn2SearchAFunction(columns2Search, columnsDefs)
39
-
40
- if (resolveLocalizedDataAtRuntime) {
30
+ // Resolve localized data at runtime if the localized association is defined for the target entity.
31
+ // Notice that if the localized association is defined, there should be at least one localized element.
32
+ if (localizedAssociation) {
41
33
  const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
42
34
 
43
35
  // REVISIT this is dirty but works for now
44
36
  // replace $user_locale placeholder with the user locale or the HANA session context
45
37
  onCondition[0].xpr[onCondition[0].xpr.length - 1] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
46
38
 
47
- // inner join the target table with the _texts table (the _texts table contains
48
- // the translated texts)
39
+ // inner join the target table with the _texts table (the _texts table contains the translated texts)
49
40
  const localizedEntityName = localizedAssociation.target
50
41
  query.join(localizedEntityName).on(onCondition)
51
42
 
52
43
  // prevent SQL ambiguity error for columns with the same name
53
44
  columns2Search = _addAliasToQuery(query, entity, columns2Search)
45
+
46
+ // suppress the localize handler from redirecting the query's target to the localized view
47
+ Object.defineProperty(query, '_suppressLocalization', { value: true })
54
48
  } // else --> resolve localized texts via localized view (default)
55
49
 
56
- const useContains = !isSomeColumn2SearchUseFunction && isContainsPredicateSupported(query)
50
+ const useContains = !!localizedAssociation && isContainsPredicateSupported(query, entity, columns2Search)
57
51
  let expression
58
52
 
59
53
  if (useContains) {
@@ -70,23 +64,12 @@ const search2cqn4sql = (query, entity, options) => {
70
64
  return query
71
65
  }
72
66
 
73
- const _isSomeColumn2SearchAFunction = (columns2Search, columnsDefs) =>
74
- columns2Search.some(column2Search => {
75
- if (column2Search.func) return true
76
- return columnsDefs && columnsDefs.some(columnDef => columnDef.func && columnDef.as === column2Search.ref[0])
77
- })
78
-
79
- const _getLocalizedAssociation = entity => {
80
- const associations = entity.associations
81
- return associations && associations.localized
82
- }
83
-
84
67
  // The inner join modifies the original SELECT ... FROM query and adds ambiguity,
85
68
  // therefore add the table/entity name (as a preceding element) to the columns ref
86
69
  // to prevent a SQL ambiguity error.
87
70
  const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
88
71
  const SELECT = query.SELECT
89
- const localizedEntityName = _getLocalizedAssociation(entity).target
72
+ const localizedEntityName = entity.associations?.localized.target
90
73
  const elements = entity.elements
91
74
  const entityName = entity.name
92
75
  const getEntityName = columnRef => {
@@ -9,6 +9,7 @@ const STREAM_PLACEHOLDER = '[<stream>]'
9
9
  const _loadStreamExtensionIfNeeded = () => {
10
10
  const hana = require('./driver')
11
11
  if (hana.name !== 'hdb') {
12
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
12
13
  const extension = require('@sap/hana-client/extension/Stream.js')
13
14
  return isDynatraceEnabled() ? dynatraceStreamingExtension(extension) : extension
14
15
  }
@@ -39,8 +39,10 @@ class AMQPWebhookMessaging extends MessagingService {
39
39
 
40
40
  startListening(opt = {}) {
41
41
  if (!this.subscribedTopics.size) return
42
- const management = this.getManagement()
43
- if (!opt.doNotDeploy) this.queued(management.createQueueAndSubscriptions.bind(management))()
42
+ if (!opt.doNotDeploy) {
43
+ const management = this.getManagement()
44
+ this.queued(management.createQueueAndSubscriptions.bind(management))()
45
+ }
44
46
  this.queued(this.listenToClient.bind(this))(async (_topic, _payload, _other, { done, failed }) => {
45
47
  const msg = Object.assign(normalizeIncomingMessage(_payload), _other || {})
46
48
  msg.event = _topic
@@ -1,4 +1,5 @@
1
1
  const cds = require('../../cds.js')
2
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
2
3
  const ClientAmqp = require('@sap/xb-msg-amqp-v100').Client
3
4
  const { connect, disconnect } = require('./connections')
4
5
  const { hasPersistentOutbox } = require('../outbox/utils')
@@ -2,11 +2,14 @@ const cds = require('../../cds')
2
2
  const _transform = o => ({ subdomain: o.subscribedSubdomain, tenant: o.subscribedTenantId })
3
3
 
4
4
  const getTenantInfo = async tenant => {
5
- const provisioning = await cds.connect.to('ProvisioningService')
5
+ const provisioningServiceName = cds.mtx ? 'ProvisioningService' : 'cds.xt.SaasProvisioningService'
6
+ const primaryKey = cds.mtx ? 'ID' : 'subscribedTenantId'
7
+
8
+ const provisioning = await cds.connect.to(provisioningServiceName)
6
9
  const tx = provisioning.tx({ user: new cds.User.Privileged() })
7
10
  try {
8
11
  const result = tenant
9
- ? _transform(await tx.get(`tenant`, { ID: tenant }))
12
+ ? _transform(await tx.get(`tenant`, { [primaryKey]: tenant }))
10
13
  : (await tx.read('tenant')).map(o => _transform(o))
11
14
  await tx.commit()
12
15
  return result
@@ -1,4 +1,5 @@
1
1
  const cds = require('../../cds.js')
2
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
2
3
  const express = require('express')
3
4
  const getTenantInfo = require('./getTenantInfo.js')
4
5
  const isSecured = () => cds.requires.auth && cds.requires.auth.credentials
@@ -14,6 +15,7 @@ class EndpointRegistry {
14
15
  this.deployCallbacks = new Map()
15
16
  if (isSecured()) {
16
17
  const JWTStrategy = require('../../auth/strategies/JWT.js')
18
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
17
19
  const passport = require('passport')
18
20
  // REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
19
21
  // In principle, user-facing endpoints might differ from messaging ones.
@@ -5,6 +5,7 @@ const optionsManagement = require('./enterprise-messaging-utils/options-manageme
5
5
  const EMManagement = require('./enterprise-messaging-utils/EMManagement.js')
6
6
  const optionsForSubdomain = require('./common-utils/optionsForSubdomain.js')
7
7
  const authorizedRequest = require('./common-utils/authorizedRequest')
8
+ const getTenantInfo = require('./enterprise-messaging-utils/getTenantInfo')
8
9
  const sleep = require('util').promisify(setTimeout)
9
10
  const {
10
11
  registerDeployEndpoints,
@@ -26,6 +27,9 @@ const _checkAppURL = appURL => {
26
27
  )
27
28
  }
28
29
 
30
+ const _oldMtx = () => cds.mtx
31
+ const _multitenancyEnabled = () => cds.requires.multitenancy || _oldMtx()
32
+
29
33
  // REVISIT: It's bad to have to rely on the subdomain.
30
34
  // For all interactions where we perform the token exchange ourselves,
31
35
  // we will be able to use the zoneId instead of the subdomain.
@@ -77,8 +81,62 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
77
81
  })
78
82
  }
79
83
 
84
+ // New mtx based on @sap/cds-mtxs
85
+ async addMTXSHandlers() {
86
+ const deploymentSrv = await cds.connect.to('cds.xt.DeploymentService')
87
+ const provisioningSrv = await cds.connect.to('cds.xt.SaasProvisioningService')
88
+ deploymentSrv.impl(() => {
89
+ deploymentSrv.on('subscribe', async (req, next) => {
90
+ const res = await next()
91
+ const { tenant } = req.data
92
+ let subdomain
93
+ try {
94
+ const tenantInfo = await getTenantInfo(tenant) // @sap/cds-mtxs must provide that info
95
+ subdomain = tenantInfo.subdomain
96
+ } catch (e) {
97
+ this.LOG.error("'subscribe' is not yet implemented for @sap/cds-mtxs")
98
+ throw e
99
+ }
100
+ const management = await this.getManagement(subdomain).waitUntilReady()
101
+ await management.deploy()
102
+ return res
103
+ })
104
+ deploymentSrv.on('unsubscribe', async (req, next) => {
105
+ const res = await next()
106
+ const { tenant } = req.data
107
+ let subdomain
108
+ try {
109
+ const tenantInfo = await getTenantInfo(tenant) // @sap/cds-mtxs must provide that info
110
+ subdomain = tenantInfo.subdomain
111
+ } catch (e) {
112
+ this.LOG.error("'unsubscribe' is not yet implemented for @sap/cds-mtxs")
113
+ throw e
114
+ }
115
+ try {
116
+ const management = await this.getManagement(subdomain).waitUntilReady()
117
+ await management.undeploy()
118
+ } catch (error) {
119
+ this.LOG.error('Failed to delete messaging artifacts for subdomain', subdomain, '(', error, ')')
120
+ }
121
+ return res
122
+ })
123
+ })
124
+ provisioningSrv.impl(() => {
125
+ provisioningSrv.on('dependencies', async (req, next) => {
126
+ this.LOG._info && this.LOG.info('Include Enterprise-Messaging as SaaS dependency')
127
+ const res = (await next()) || []
128
+ const xsappname = this.options.credentials?.xsappname
129
+ if (xsappname) {
130
+ const exists = res.some(d => d.xsappname === xsappname)
131
+ if (!exists) res.push({ xsappname })
132
+ }
133
+ return res
134
+ })
135
+ })
136
+ }
137
+
80
138
  startListening() {
81
- const doNotDeploy = cds.mtx && !this.options.deployForProvider
139
+ const doNotDeploy = _multitenancyEnabled() && !this.options.deployForProvider
82
140
  if (doNotDeploy) this.LOG._info && this.LOG.info('Skipping deployment of messaging artifacts for provider account')
83
141
  super.startListening({ doNotDeploy })
84
142
  if (!doNotDeploy && this.subscribedTopics.size) {
@@ -97,8 +155,9 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
97
155
  async listenToClient(cb) {
98
156
  _checkAppURL(this.optionsApp.appURL)
99
157
  registerWebhookEndpoints(BASE_PATH, this.queueName, this.LOG, cb)
100
- if (cds.mtx) {
101
- await this.addMTXHandlers()
158
+ if (_multitenancyEnabled()) {
159
+ if (_oldMtx()) await this.addMTXHandlers()
160
+ else await this.addMTXSHandlers()
102
161
  registerDeployEndpoints(BASE_PATH, this.queueName, async (tenantInfo, options) => {
103
162
  const result = { queue: this.queueName, succeeded: [], failed: [] }
104
163
  await Promise.all(
@@ -32,7 +32,7 @@ const hasPersistentOutbox = (srv, tenant) => {
32
32
  if (!cds.requires.outbox || cds.requires.outbox.kind !== 'persistent-outbox') return false
33
33
  if (srv.options && srv.options.outbox && srv.options.outbox.kind && srv.options.outbox.kind !== 'persistent-outbox')
34
34
  return false
35
- if (cds.mtx && tenant && _isProviderTenant(tenant)) return false // no persistence for provider account
35
+ if ((cds.mtx || cds.requires.multitenancy) && tenant && _isProviderTenant(tenant)) return false // no persistence for provider account
36
36
  return true
37
37
  }
38
38
 
@@ -1,3 +1,4 @@
1
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
1
2
  const redis = require('redis')
2
3
  const cds = require('../../../lib')
3
4
  const waitingTime = require('./common-utils/waitingTime')
@@ -6,6 +6,7 @@ const LOG = cds.log('remote')
6
6
  // disable sdk logger if not in debug mode
7
7
  if (!LOG._debug) {
8
8
  try {
9
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
9
10
  const sdkUtils = require('@sap-cloud-sdk/util')
10
11
  sdkUtils.setGlobalLogLevel('error')
11
12
  } catch (err) {
@@ -143,8 +144,7 @@ const _addHandlerActionFunction = (srv, def, target) => {
143
144
  if (target) {
144
145
  srv.on(event, target, async function (req) {
145
146
  const shortEntityName = req.target.name.replace(`${this.namespace}.`, '')
146
- if (this.kind === 'odata-v2')
147
- return _handleV2BoundActionFunction(srv, def, req, `${shortEntityName}_${event}`, this.kind)
147
+ if (this.kind === 'odata-v2') return _handleV2BoundActionFunction(srv, def, req, event, this.kind)
148
148
  const url = `/${shortEntityName}(${_buildKeys(req, this.kind).join(',')})/${this.namespace}.${event}`
149
149
  return _handleBoundActionFunction(srv, def, req, url)
150
150
  })
@@ -24,8 +24,8 @@ const _sanitizeHeaders = headers => {
24
24
 
25
25
  const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
26
26
  const { executeHttpRequestWithOrigin } = cloudSdk()
27
-
28
27
  const destinationName = typeof destination === 'string' && destination
28
+
29
29
  if (destinationName) {
30
30
  destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) || {}) }
31
31
  } else if (destination.forwardAuthToken) {
@@ -59,13 +59,21 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
59
59
 
60
60
  // cloud sdk requires a new mechanism to differentiate the priority of headers
61
61
  // "custom" keeps the highest priority as before
62
- requestConfig = { ...requestConfig, headers: { custom: { ...requestConfig.headers } } }
62
+ const maxBodyLength = cds.env?.remote?.max_body_length
63
+ requestConfig = {
64
+ ...requestConfig,
65
+ headers: {
66
+ custom: { ...requestConfig.headers }
67
+ },
68
+ ...(maxBodyLength && { maxBodyLength })
69
+ }
63
70
 
64
71
  return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
65
72
  }
66
73
 
67
74
  const cloudSdk = () => {
68
75
  if (_cloudSdk) return _cloudSdk
76
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
69
77
  _cloudSdk = require('@sap-cloud-sdk/http-client')
70
78
  return _cloudSdk
71
79
  }
@@ -145,10 +153,12 @@ function _defineProperty(obj, property, value) {
145
153
  const map = (..._) => _defineProperty(_map.call(obj, ..._), property, value)
146
154
  props.map = { value: map, enumerable: false, configurable: true, writable: true }
147
155
  }
156
+
148
157
  props[property] = { value: value, enumerable: false, configurable: true, writable: true }
149
158
  for (const prop in props) {
150
159
  Object.defineProperty(obj, prop, props[prop])
151
160
  }
161
+
152
162
  return obj
153
163
  }
154
164
 
@@ -156,14 +166,17 @@ function _normalizeMetadata(prefix, data, results) {
156
166
  const target = results !== undefined ? results : data
157
167
  if (typeof target !== 'object' || target === null) return target
158
168
  const metadataKeys = Object.keys(data).filter(k => prefix.test(k))
169
+
159
170
  for (const k of metadataKeys) {
160
171
  const $ = k.replace(prefix, '$')
161
172
  _defineProperty(target, $, data[k])
162
173
  delete target[k]
163
174
  }
175
+
164
176
  if (Array.isArray(target)) {
165
177
  return target.map(row => _normalizeMetadata(prefix, row))
166
178
  }
179
+
167
180
  // check properties for all and prop.results for odata v2
168
181
  for (const [key, value] of Object.entries(target)) {
169
182
  if (value && typeof value === 'object') {
@@ -171,6 +184,7 @@ function _normalizeMetadata(prefix, data, results) {
171
184
  target[key] = _normalizeMetadata(prefix, value, nestedResults)
172
185
  }
173
186
  }
187
+
174
188
  return target
175
189
  }
176
190
  const _getPurgedRespActionFunc = (data, returnType) => {
@@ -181,6 +195,7 @@ const _getPurgedRespActionFunc = (data, returnType) => {
181
195
  return data[key]
182
196
  }
183
197
  }
198
+
184
199
  return data
185
200
  }
186
201
 
@@ -198,6 +213,7 @@ const _purgeODataV2 = (data, target, returnType, reqHeaders) => {
198
213
  ieee754Compatible,
199
214
  exponentialDecimals
200
215
  )
216
+
201
217
  return _normalizeMetadata(/^__/, data, convertedResponse)
202
218
  }
203
219
 
@@ -217,6 +233,7 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
217
233
  url: e.config ? e.config.baseURL + e.config.url : reqOptions.url,
218
234
  headers: e.config ? e.config.headers : reqOptions.headers
219
235
  }
236
+
220
237
  if (options.batchRequest) {
221
238
  e.request.body = reqOptions.data
222
239
  }
@@ -227,9 +244,11 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
227
244
  statusText: e.response.statusText,
228
245
  headers: e.response.headers
229
246
  }
247
+
230
248
  if (e.response.data && !options.suppressRemoteResponseBody) {
231
249
  response.body = e.response.data
232
250
  }
251
+
233
252
  e.response = response
234
253
  }
235
254
 
@@ -270,12 +289,7 @@ const run = async (
270
289
  response = await _executeHttpRequest({ requestConfig, destination, destinationOptions, jwt })
271
290
  } catch (e) {
272
291
  // > axios received status >= 400 -> gateway error
273
- const msg =
274
- (e.response &&
275
- e.response.data &&
276
- e.response.data.error &&
277
- ((e.response.data.error.message && e.response.data.error.message.value) || e.response.data.error.message)) ||
278
- e.message
292
+ const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
279
293
  e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
280
294
 
281
295
  const sanitizedError = _getSanitizedError(e, requestConfig, {
@@ -284,7 +298,6 @@ const run = async (
284
298
 
285
299
  const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
286
300
  LOG._warn && LOG.warn(err)
287
-
288
301
  throw err
289
302
  }
290
303
 
@@ -312,7 +325,6 @@ const run = async (
312
325
  })
313
326
 
314
327
  LOG._warn && LOG.warn(err)
315
-
316
328
  throw err
317
329
  }
318
330
 
@@ -331,6 +343,7 @@ const run = async (
331
343
  if (responseDataSplitted[1].startsWith('HTTP/1.1 2')) {
332
344
  response.data = contentJSON
333
345
  }
346
+
334
347
  if (responseDataSplitted[1].startsWith('HTTP/1.1 4') || responseDataSplitted[1].startsWith('HTTP/1.1 5')) {
335
348
  const innerError = contentJSON.error || contentJSON
336
349
  innerError.status = Number(responseDataSplitted[1].match(/HTTP.*(\d{3})/m)[1])
@@ -357,6 +370,7 @@ const run = async (
357
370
  }
358
371
  return _purgeODataV4(response.data)
359
372
  }
373
+
360
374
  return response.data
361
375
  }
362
376
 
@@ -368,6 +382,7 @@ const getJwt = req => {
368
382
  return token[1]
369
383
  }
370
384
  }
385
+
371
386
  return null
372
387
  }
373
388
 
@@ -382,9 +397,11 @@ const _cqnToReqOptions = (query, service, req) => {
382
397
  .replace(/\( /g, '(')
383
398
  .replace(/ \)/g, ')')
384
399
  }
400
+
385
401
  if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
386
402
  reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, req.target) : queryObject.body
387
403
  }
404
+
388
405
  return reqOptions
389
406
  }
390
407
 
@@ -395,9 +412,11 @@ const _stringToReqOptions = (query, data, target) => {
395
412
  method: cleanQuery.substring(0, blankIndex).toUpperCase(),
396
413
  url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
397
414
  }
415
+
398
416
  if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
399
417
  reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
400
418
  }
419
+
401
420
  return reqOptions
402
421
  }
403
422
 
@@ -412,10 +431,12 @@ const _pathToReqOptions = (method, path, data, target) => {
412
431
  // normalize in case parts[2] already starts with /
413
432
  url = url.replace(/^\/\//, '/')
414
433
  }
434
+
415
435
  const reqOptions = { method, url }
416
436
  if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
417
437
  reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
418
438
  }
439
+
419
440
  return reqOptions
420
441
  }
421
442
 
@@ -453,8 +474,11 @@ const getReqOptions = (req, query, service) => {
453
474
  if (typeof reqOptions.data === 'object' && !Buffer.isBuffer(reqOptions.data)) {
454
475
  reqOptions.headers['content-type'] = 'application/json'
455
476
  reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
456
- } else if (typeof reqOptions.data === 'string' || Buffer.isBuffer(reqOptions.data)) {
477
+ } else if (typeof reqOptions.data === 'string') {
478
+ reqOptions.headers['content-length'] = Buffer.byteLength(reqOptions.data)
479
+ } else if (Buffer.isBuffer(reqOptions.data)) {
457
480
  reqOptions.headers['content-length'] = Buffer.byteLength(reqOptions.data)
481
+ if (!_hasHeader(req.headers, 'content-type')) reqOptions.headers['content-type'] = 'application/octet-stream'
458
482
  }
459
483
  }
460
484
  reqOptions.url = formatPath(reqOptions.url)
@@ -78,9 +78,14 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
78
78
  } else if (value === 'false') {
79
79
  value = false
80
80
  }
81
- } else if (type === 'cds.Integer') {
81
+ } else if (type === 'cds.Integer' || type === 'cds.UInt8' || type === 'cds.Int16' || type === 'cds.Int32') {
82
82
  value = parseInt(value, 10)
83
- } else if (type === 'cds.Decimal' || type === 'cds.DecimalFloat' || type === 'cds.Integer64') {
83
+ } else if (
84
+ type === 'cds.Decimal' ||
85
+ type === 'cds.DecimalFloat' ||
86
+ type === 'cds.Integer64' ||
87
+ type === 'cds.Int64'
88
+ ) {
84
89
  const bigValue = big(value)
85
90
  if (ieee754Compatible) {
86
91
  // TODO test with arrayed => element.items.scale?
@@ -17,6 +17,7 @@ const execute = require('./execute')
17
17
 
18
18
  const _new = url => {
19
19
  if (url && url !== ':memory:') url = cds.utils.path.resolve(cds.root, url)
20
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
20
21
  if (!_sqlite) _sqlite = require('sqlite3')
21
22
  return new Promise((resolve, reject) => {
22
23
  const dbc = new _sqlite.Database(url, err => {
@@ -102,8 +103,8 @@ module.exports = class SQLiteDatabase extends DatabaseService {
102
103
  const credentials = this.options.credentials || this.options || {}
103
104
  let dbUrl = credentials.database || credentials.url || credentials.host || ':memory:'
104
105
 
105
- if (tenant && dbUrl.endsWith('.db')) {
106
- dbUrl = dbUrl.split('.db')[0] + '_' + tenant + '.db'
106
+ if (tenant && dbUrl !== ':memory:') {
107
+ dbUrl = dbUrl.replace(/\.(db|sqlite)$/, `-${tenant}.$1`)
107
108
  }
108
109
  return dbUrl
109
110
  }
@@ -153,13 +154,13 @@ module.exports = class SQLiteDatabase extends DatabaseService {
153
154
  */
154
155
  // REVISIT: make tenant aware
155
156
  async deploy(model, options = {}) {
156
- const createEntities = cds.compile.to.sql(model, options)
157
- if (!createEntities || createEntities.length === 0) return // > nothing to deploy
157
+ let createEntities = cds.compile.to.sql(model, options)
158
+ if (createEntities.length === 0) return // > nothing to deploy
158
159
 
159
- const dropViews = []
160
- const dropTables = []
160
+ let dropViews = []
161
+ let dropTables = []
161
162
  for (const each of createEntities) {
162
- const [, table, entity] = each.match(/^\s*CREATE (?:(TABLE)|VIEW)\s+"?([^\s"(]+)"?/im) || []
163
+ const [, table, entity] = each.match(/^CREATE (?:(TABLE)|VIEW)\s+"?([^\s"(]+)"?/im) || []
163
164
  if (table) dropTables.push({ DROP: { entity } })
164
165
  else dropViews.push({ DROP: { view: entity } })
165
166
  }
@@ -190,6 +191,12 @@ module.exports = class SQLiteDatabase extends DatabaseService {
190
191
  await this.run(async tx => {
191
192
  // This starts a new transaction if called from CLI, while joining
192
193
  // existing root tx, e.g. when called from DeploymenrService
194
+ const [ext] = await tx.run(`SELECT 1 from sqlite_master where name='cds_xt_Extensions'`)
195
+ if (ext) {
196
+ // Poor man's schema evolution for MTX upgrade operations
197
+ createEntities = createEntities.filter(ct => !ct.match(/^CREATE TABLE cds_xt_Extensions/im))
198
+ dropTables = dropTables.filter(dt => dt.DROP.entity !== 'cds_xt_Extensions')
199
+ }
193
200
  await tx.run(dropViews)
194
201
  await tx.run(dropTables)
195
202
  await tx.run(createEntities)
@@ -197,4 +204,8 @@ module.exports = class SQLiteDatabase extends DatabaseService {
197
204
 
198
205
  return true
199
206
  }
207
+
208
+ async disconnect(tenant) {
209
+ this.dbcs.delete(tenant)
210
+ }
200
211
  }
@@ -39,15 +39,18 @@ const SQLITE_TYPE_CONVERSION_MAP = new Map([
39
39
  ['cds.Boolean', convertToBoolean],
40
40
  ['cds.Date', convertToDateString],
41
41
  ['cds.Integer64', convertInt64ToString],
42
+ ['cds.Int64', convertInt64ToString],
42
43
  ['cds.DateTime', convertToISONoMillis],
43
44
  ['cds.Timestamp', convertToISOTime]
44
45
  ])
45
46
 
46
47
  if (cds.env.features.bigjs) {
48
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
47
49
  const Big = require('big.js')
48
50
  const convertToBig = value => new Big(value)
49
51
 
50
52
  SQLITE_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
53
+ SQLITE_TYPE_CONVERSION_MAP.set('cds.Int64', convertToBig)
51
54
  SQLITE_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
52
55
  }
53
56