@sap/cds 5.5.5 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/apis/services.d.ts +27 -1
  3. package/app/index.js +22 -11
  4. package/bin/build/buildTaskFactory.js +1 -1
  5. package/bin/build/provider/buildTaskProviderInternal.js +1 -1
  6. package/bin/build/provider/fiori/index.js +1 -1
  7. package/bin/build/provider/hana/2migration.js +8 -7
  8. package/bin/build/provider/java-cf/index.js +1 -1
  9. package/bin/deploy/to-hana/hana.js +1 -17
  10. package/common.cds +8 -0
  11. package/lib/compile/to/sql.js +22 -2
  12. package/lib/connect/bindings.js +2 -1
  13. package/lib/core/reflect.js +3 -1
  14. package/lib/env/index.js +175 -41
  15. package/lib/env/requires.js +16 -1
  16. package/lib/i18n/localize.js +31 -4
  17. package/lib/index.js +3 -3
  18. package/lib/log/format/kibana.js +6 -2
  19. package/lib/ql/Query.js +1 -0
  20. package/lib/ql/SELECT.js +15 -8
  21. package/lib/ql/Whereable.js +5 -0
  22. package/lib/req/context.js +13 -5
  23. package/lib/serve/Service-dispatch.js +8 -1
  24. package/lib/utils/axios.js +7 -0
  25. package/lib/utils/data.js +1 -1
  26. package/lib/utils/tests.js +1 -1
  27. package/libx/_runtime/audit/Service.js +18 -18
  28. package/libx/_runtime/audit/generic/personal/access.js +1 -1
  29. package/libx/_runtime/audit/generic/personal/modification.js +3 -2
  30. package/libx/_runtime/audit/generic/personal/utils.js +23 -63
  31. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +4 -0
  32. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +37 -35
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +3 -1
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +5 -5
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +13 -7
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +84 -34
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +10 -4
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +9 -3
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +8 -6
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -3
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +13 -11
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +11 -95
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +17 -11
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +6 -2
  46. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +3 -34
  47. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -3
  48. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +48 -18
  49. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +10 -5
  50. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +1 -3
  53. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +14 -19
  54. package/libx/_runtime/cds-services/services/utils/columns.js +6 -1
  55. package/libx/_runtime/cds-services/services/utils/compareJson.js +1 -8
  56. package/libx/_runtime/cds-services/services/utils/differ.js +7 -26
  57. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +2 -4
  58. package/libx/_runtime/cds-services/util/assert.js +29 -13
  59. package/libx/_runtime/cds.js +2 -1
  60. package/libx/_runtime/common/aspects/Association.js +72 -0
  61. package/libx/_runtime/common/aspects/any.js +8 -45
  62. package/libx/_runtime/common/aspects/entity.js +0 -1
  63. package/libx/_runtime/common/aspects/relation.js +40 -0
  64. package/libx/_runtime/common/aspects/utils.js +73 -1
  65. package/libx/_runtime/common/auth/strategies/utils/uaa.js +1 -12
  66. package/libx/_runtime/common/composition/data.js +3 -2
  67. package/libx/_runtime/common/composition/delete.js +3 -1
  68. package/libx/_runtime/common/composition/tree.js +23 -18
  69. package/libx/_runtime/common/composition/utils.js +34 -8
  70. package/libx/_runtime/common/error/frontend.js +6 -1
  71. package/libx/_runtime/common/generic/auth.js +5 -9
  72. package/libx/_runtime/common/generic/crud.js +2 -2
  73. package/libx/_runtime/common/generic/etag.js +11 -8
  74. package/libx/_runtime/common/generic/input.js +3 -3
  75. package/libx/_runtime/common/generic/paging.js +9 -5
  76. package/libx/_runtime/common/generic/put.js +3 -2
  77. package/libx/_runtime/common/generic/sorting.js +3 -3
  78. package/libx/_runtime/common/generic/temporal.js +3 -3
  79. package/libx/_runtime/common/utils/cqn.js +20 -1
  80. package/libx/_runtime/common/utils/cqn2cqn4sql.js +125 -139
  81. package/libx/_runtime/common/utils/csn.js +50 -52
  82. package/libx/_runtime/common/utils/foreignKeyPropagations.js +41 -176
  83. package/libx/_runtime/common/utils/generateOnCond.js +40 -70
  84. package/libx/_runtime/common/utils/{enrichWithKeysFromWhere.js → keys.js} +29 -28
  85. package/libx/_runtime/common/utils/postProcessing.js +3 -0
  86. package/libx/_runtime/common/utils/propagateForeignKeys.js +84 -0
  87. package/libx/_runtime/common/utils/resolveStructured.js +1 -1
  88. package/libx/_runtime/common/utils/resolveView.js +7 -5
  89. package/libx/_runtime/common/utils/rewriteAsterisks.js +94 -0
  90. package/libx/_runtime/common/utils/search2cqn4sql.js +9 -8
  91. package/libx/_runtime/common/utils/template.js +54 -46
  92. package/libx/_runtime/db/Service.js +9 -2
  93. package/libx/_runtime/db/expand/expandCQNToJoin.js +10 -24
  94. package/libx/_runtime/db/expand/rawToExpanded.js +2 -1
  95. package/libx/_runtime/db/generic/create.js +1 -0
  96. package/libx/_runtime/db/generic/input.js +7 -11
  97. package/libx/_runtime/db/generic/integrity.js +2 -2
  98. package/libx/_runtime/db/generic/rewrite.js +2 -5
  99. package/libx/_runtime/db/generic/update.js +1 -0
  100. package/libx/_runtime/db/query/read.js +9 -4
  101. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -2
  102. package/libx/_runtime/db/sql-builder/annotations.js +1 -0
  103. package/libx/_runtime/db/utils/columns.js +14 -43
  104. package/libx/_runtime/fiori/generic/activate.js +3 -2
  105. package/libx/_runtime/fiori/generic/before.js +2 -2
  106. package/libx/_runtime/fiori/generic/cancel.js +3 -2
  107. package/libx/_runtime/fiori/generic/delete.js +3 -2
  108. package/libx/_runtime/fiori/generic/edit.js +2 -2
  109. package/libx/_runtime/fiori/generic/new.js +2 -2
  110. package/libx/_runtime/fiori/generic/patch.js +2 -2
  111. package/libx/_runtime/fiori/generic/prepare.js +2 -2
  112. package/libx/_runtime/fiori/generic/read.js +17 -63
  113. package/libx/_runtime/fiori/generic/readOverDraft.js +4 -4
  114. package/libx/_runtime/fiori/uiflex/extensibility/index.cds +15 -0
  115. package/libx/_runtime/fiori/uiflex/extensibility/index.js +148 -0
  116. package/libx/_runtime/fiori/uiflex/handler/transformREAD.js +119 -0
  117. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +43 -0
  118. package/libx/_runtime/fiori/uiflex/handler/transformWRITE.js +62 -0
  119. package/libx/_runtime/fiori/uiflex/index.js +35 -0
  120. package/libx/_runtime/fiori/uiflex/utils.js +78 -0
  121. package/libx/_runtime/fiori/utils/handler.js +3 -13
  122. package/libx/_runtime/fiori/utils/where.js +6 -1
  123. package/libx/_runtime/hana/pool.js +12 -11
  124. package/libx/_runtime/hana/search2cqn4sql.js +34 -43
  125. package/libx/_runtime/hana/searchToContains.js +3 -3
  126. package/libx/_runtime/index.js +5 -2
  127. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  128. package/libx/_runtime/messaging/common-utils/AMQPClient.js +9 -1
  129. package/libx/_runtime/messaging/common-utils/connections.js +11 -14
  130. package/libx/_runtime/messaging/common-utils/naming-conventions.js +1 -1
  131. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -1
  132. package/libx/_runtime/messaging/message-queuing.js +18 -0
  133. package/libx/_runtime/remote/Service.js +14 -2
  134. package/libx/_runtime/remote/utils/client-types.d.ts +7 -0
  135. package/libx/_runtime/remote/utils/client.js +117 -23
  136. package/libx/_runtime/sqlite/Service.js +2 -2
  137. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +1 -3
  138. package/libx/gql/GraphQLAdapter.js +33 -0
  139. package/libx/gql/constants/adapter.js +69 -0
  140. package/libx/gql/constants/cds.js +18 -0
  141. package/libx/gql/constants/graphql.js +33 -0
  142. package/libx/gql/resolvers/crud/create.js +15 -0
  143. package/libx/gql/resolvers/crud/delete.js +24 -0
  144. package/libx/gql/resolvers/crud/index.js +6 -0
  145. package/libx/gql/resolvers/crud/read.js +25 -0
  146. package/libx/gql/resolvers/crud/update.js +31 -0
  147. package/libx/gql/resolvers/crud/utils/index.js +36 -0
  148. package/libx/gql/resolvers/field.js +5 -0
  149. package/libx/gql/resolvers/index.js +7 -0
  150. package/libx/gql/resolvers/mutation.js +23 -0
  151. package/libx/gql/resolvers/parse/ast/enrich.js +51 -0
  152. package/libx/gql/resolvers/parse/ast/fragment.js +11 -0
  153. package/libx/gql/resolvers/parse/ast/fromObject.js +39 -0
  154. package/libx/gql/resolvers/parse/ast/index.js +3 -0
  155. package/libx/gql/resolvers/parse/ast/meta.js +4 -0
  156. package/libx/gql/resolvers/parse/ast/variable.js +7 -0
  157. package/libx/gql/resolvers/parse/ast2cqn/columns.js +42 -0
  158. package/libx/gql/resolvers/parse/ast2cqn/entries.js +31 -0
  159. package/libx/gql/resolvers/parse/ast2cqn/index.js +8 -0
  160. package/libx/gql/resolvers/parse/ast2cqn/limit.js +6 -0
  161. package/libx/gql/resolvers/parse/ast2cqn/orderBy.js +24 -0
  162. package/libx/gql/resolvers/parse/ast2cqn/utils/index.js +3 -0
  163. package/libx/gql/resolvers/parse/ast2cqn/where.js +70 -0
  164. package/libx/gql/resolvers/parse/utils/index.js +8 -0
  165. package/libx/gql/resolvers/query.js +13 -0
  166. package/libx/gql/resolvers/root.js +34 -0
  167. package/libx/gql/schema/generate.js +18 -0
  168. package/libx/gql/schema/index.js +5 -0
  169. package/libx/gql/schema/mutation.js +76 -0
  170. package/libx/gql/schema/query.js +108 -0
  171. package/libx/gql/schema/typeDefMap.js +45 -0
  172. package/libx/gql/schema/utils/index.js +54 -0
  173. package/libx/gql/utils/index.js +12 -0
  174. package/libx/{_runtime/odata/cqn2odata.js → odata/cqn2odata/index.js} +39 -100
  175. package/libx/odata/index.js +80 -0
  176. package/libx/odata/odata2cqn/afterburner.js +170 -0
  177. package/libx/{_runtime/odata/odata2cqn.pegjs → odata/odata2cqn/grammar.pegjs} +102 -123
  178. package/libx/odata/odata2cqn/index.js +3 -0
  179. package/libx/odata/odata2cqn/parser.js +1 -0
  180. package/libx/odata/utils/index.js +64 -0
  181. package/libx/rest/RestAdapter.js +101 -0
  182. package/libx/rest/RestRequest.js +30 -0
  183. package/libx/rest/index.js +3 -0
  184. package/libx/rest/middleware/auth.js +22 -0
  185. package/libx/rest/middleware/content.js +15 -0
  186. package/libx/rest/middleware/create.js +40 -0
  187. package/libx/rest/middleware/delete.js +20 -0
  188. package/libx/rest/middleware/error.js +56 -0
  189. package/libx/rest/middleware/operation.js +39 -0
  190. package/libx/rest/middleware/parse.js +90 -0
  191. package/libx/rest/middleware/read.js +29 -0
  192. package/libx/rest/middleware/update.js +42 -0
  193. package/libx/rest/utils/data.js +65 -0
  194. package/package.json +4 -1
  195. package/server.js +20 -2
  196. package/libx/_runtime/cds-services/services/utils/diff.js +0 -53
  197. package/libx/_runtime/cds-services/util/auditlog.js +0 -247
  198. package/libx/_runtime/cds-services/util/xsenv.js +0 -51
  199. package/libx/_runtime/common/utils/backlinks.js +0 -83
  200. package/libx/_runtime/common/utils/rewriteAsterisk.js +0 -72
  201. package/libx/_runtime/odata/index.js +0 -55
  202. package/libx/_runtime/odata/odata2cqn.js +0 -1
  203. package/libx/_runtime/odata/readToCqn.js +0 -129
  204. package/libx/_runtime/remote/cqn2odata/index.js +0 -2
@@ -1,7 +1,6 @@
1
1
  const { computeColumnsToBeSearched } = require('../cds-services/services/utils/columns')
2
2
  const searchToLike = require('../common/utils/searchToLike')
3
3
  const { isContainsPredicateSupported, searchToContains } = require('./searchToContains')
4
- const { getOnCond } = require('../common/utils/generateOnCond')
5
4
 
6
5
  /**
7
6
  * Computes a CQN expression for a search query.
@@ -15,30 +14,27 @@ const { getOnCond } = require('../common/utils/generateOnCond')
15
14
  * But in contrast to the explicitly written `LIKE ?`, the parameter is already resolved to its concrete value, making
16
15
  * it better optimizable by the HANA optimizer.
17
16
  *
18
- * @param {object} cqn The CQN object
17
+ * @param {object} query The CQN object
19
18
  * @param {import('@sap/cds-compiler/lib/api/main').CSN} entity The target entity for the search query
20
19
  * @param {import('../types/api').search2cqnOptions} [options]
21
20
  * @returns {object} The modified CQN object
22
21
  */
23
- const search2cqn4sql = (cqn, entity, options) => {
24
- const cqnSearchPhrase = cqn.SELECT.search
25
- if (!cqnSearchPhrase) return cqn
22
+ const search2cqn4sql = (query, entity, options) => {
23
+ const cqnSearchPhrase = query.SELECT.search
24
+ if (!cqnSearchPhrase) return query
26
25
 
27
- let { columns = computeColumnsToBeSearched(cqn, entity), locale } = options
26
+ let { columns: columnsToBeSearched = computeColumnsToBeSearched(query, entity), locale } = options
28
27
  const localizedAssociation = _getLocalizedAssociation(entity)
29
28
 
30
29
  // If the localized association is defined for the target entity,
31
30
  // there should be at least one localized element.
32
31
  const resolveLocalizedTextsAtRuntime = !!localizedAssociation
33
32
 
34
- if (resolveLocalizedTextsAtRuntime) {
35
- // suppress the localize handler from modifying the from target
36
- // The `_suppressLocalization` property is:
37
- // enumerable: false (default), writable: false (default)
38
- Object.defineProperty(cqn, '_suppressLocalization', { value: true })
33
+ // suppress the localize handler from redirecting the query's target to the localized view
34
+ Object.defineProperty(query, '_suppressLocalization', { value: true })
39
35
 
40
- const onConditionOptions = _getOnConditionOptions(entity, localizedAssociation)
41
- const onCondition = getOnCond(localizedAssociation, onConditionOptions)
36
+ if (resolveLocalizedTextsAtRuntime) {
37
+ const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
42
38
 
43
39
  // replace $user_locale placeholder with the user locale or the HANA session context
44
40
  onCondition[onCondition.length - 2] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
@@ -46,31 +42,26 @@ const search2cqn4sql = (cqn, entity, options) => {
46
42
  // inner join the target table with the _texts table (the _texts table contains
47
43
  // the translated texts)
48
44
  const localizedEntityName = localizedAssociation.target
49
- cqn.join(localizedEntityName).on(onCondition)
50
-
51
- // The inner join modifies the original SELECT ... FROM query and adds ambiguity,
52
- // therefore add the table/entity name (as a preceding element) to the columns ref
53
- // to prevent a SQL ambiguity error. E.g., SqlError message: column ambiguously
54
- // defined.
55
- cqn.SELECT.columns = _unshiftEntityNameToColumnRef(entity, cqn.SELECT.columns)
56
- columns = _unshiftEntityNameToColumnRef(entity, columns)
57
- if (cqn.SELECT.groupBy) cqn.SELECT.groupBy = _unshiftEntityNameToColumnRef(entity, cqn.SELECT.groupBy)
45
+ query.join(localizedEntityName).on(onCondition)
46
+
47
+ // prevent SQL ambiguity error for columns with the same name
48
+ columnsToBeSearched = _addAliasToColumns(query, entity, columnsToBeSearched)
58
49
  } // else --> resolve localized texts via localized view (default)
59
50
 
60
- const useContains = resolveLocalizedTextsAtRuntime && isContainsPredicateSupported(cqn)
51
+ const useContains = isContainsPredicateSupported(query)
61
52
  let expression
62
53
 
63
54
  if (useContains) {
64
- expression = searchToContains(cqnSearchPhrase, columns)
55
+ expression = searchToContains(cqnSearchPhrase, columnsToBeSearched)
65
56
  } else {
66
57
  // No CONTAINS optimization possible. The search implementation for localized
67
58
  // texts falls back to the LIKE predicate.
68
- expression = searchToLike(cqnSearchPhrase, columns)
59
+ expression = searchToLike(cqnSearchPhrase, columnsToBeSearched)
69
60
  }
70
61
 
71
62
  // REVISIT: find out here if where or having must be used
72
- cqn._aggregated ? cqn.having(expression) : cqn.where(expression)
73
- return cqn
63
+ query._aggregated ? query.having(expression) : query.where(expression)
64
+ return query
74
65
  }
75
66
 
76
67
  const _getLocalizedAssociation = entity => {
@@ -78,28 +69,28 @@ const _getLocalizedAssociation = entity => {
78
69
  return associations && associations.localized
79
70
  }
80
71
 
81
- const _getOnConditionOptions = (entity, localizedAssociation) => {
82
- return {
83
- associationNames: [localizedAssociation.name],
84
- aliases: {
85
- select: localizedAssociation.target,
86
- join: entity.name
87
- }
88
- }
89
- }
90
-
91
- const _unshiftEntityNameToColumnRef = (entity, columns) => {
72
+ // The inner join modifies the original SELECT ... FROM query and adds ambiguity,
73
+ // therefore add the table/entity name (as a preceding element) to the columns ref
74
+ // to prevent a SQL ambiguity error.
75
+ const _addAliasToColumns = (query, entity, columnsToBeSearched) => {
92
76
  const localizedEntityName = _getLocalizedAssociation(entity).target
93
77
  const elements = entity.elements
94
-
95
- columns = columns.map(column => {
78
+ const entityName = entity.name
79
+ const _addAliasToColumn = (entityName, localizedEntityName, elements) => column => {
96
80
  const columnRef = column.ref
97
81
  if (!columnRef) return column
98
82
  const columnName = columnRef[0]
99
83
  const localizedElement = elements[columnName].localized
100
- const entityName = localizedElement ? localizedEntityName : entity.name
101
- return { ref: [entityName, columnName] }
102
- })
84
+ const targetEntityName = localizedElement ? localizedEntityName : entityName
85
+ return { ref: [targetEntityName, columnName] }
86
+ }
87
+
88
+ query.SELECT.columns = query.SELECT.columns.map(_addAliasToColumn(entityName, localizedEntityName, elements))
89
+ const columns = columnsToBeSearched.map(_addAliasToColumn(entityName, localizedEntityName, elements))
90
+
91
+ if (query.SELECT.groupBy) {
92
+ query.SELECT.groupBy = query.SELECT.groupBy.map(_addAliasToColumn(entityName, localizedEntityName, elements))
93
+ }
103
94
 
104
95
  return columns
105
96
  }
@@ -64,12 +64,12 @@ const searchToContains = (cqnSearchPhrase, columns) => {
64
64
  return expression
65
65
  }
66
66
 
67
- const isContainsPredicateSupported = cqn => {
68
- const cqnSearchPhrase = cqn.SELECT.search
67
+ const isContainsPredicateSupported = query => {
68
+ const cqnSearchPhrase = query.SELECT.search
69
69
 
70
70
  // REVISIT: In the future, to further optimize search queries, you might
71
71
  // want to remove the following condition(s).
72
- if (cqn._aggregated) return false
72
+ if (query._aggregated) return false
73
73
 
74
74
  // REVISIT: search terms starting with whitespace after a `NOT` operator does not
75
75
  // return the expected result on SAP HANA (BCP 2180256508). In addition, double
@@ -5,9 +5,12 @@ module.exports = {
5
5
  return this._odatav4 || (this._odatav4 = require('./cds-services/adapter/odata-v4/to'))
6
6
  },
7
7
 
8
- /** @type {import('./cds-services/adapter/rest/to')} */
9
8
  get rest() {
10
- return this._rest || (this._rest = require('./cds-services/adapter/rest/to'))
9
+ if (!this._rest) {
10
+ if (global.cds.env.features.rest_new_adapter) this._rest = require('../rest')
11
+ else this._rest = require('./cds-services/adapter/rest/to')
12
+ }
13
+ return this._rest
11
14
  }
12
15
  },
13
16
 
@@ -37,7 +37,7 @@ class AMQPWebhookMessaging extends MessagingService {
37
37
  // Some messaging systems don't adhere to the standard that the payload has a `data` property.
38
38
  // For these cases, we interpret the whole payload as `data`.
39
39
  let data, headers
40
- if ('data' in _payload) {
40
+ if (typeof _payload === 'object' && 'data' in _payload) {
41
41
  data = _payload.data
42
42
  headers = { ..._payload }
43
43
  delete headers.data
@@ -3,6 +3,14 @@ const LOG = cds.log('messaging')
3
3
  const ClientAmqp = require('@sap/xb-msg-amqp-v100').Client
4
4
  const { connect, disconnect } = require('./connections')
5
5
 
6
+ const _JSONorString = string => {
7
+ try {
8
+ return JSON.parse(string)
9
+ } catch (e) {
10
+ return string
11
+ }
12
+ }
13
+
6
14
  const addDataListener = (client, queue, prefix, cb) =>
7
15
  new Promise((resolve, reject) => {
8
16
  const source = `${prefix}${queue}`
@@ -11,7 +19,7 @@ const addDataListener = (client, queue, prefix, cb) =>
11
19
  .attach(source)
12
20
  .on('data', async raw => {
13
21
  const buffer = Buffer.concat(raw.payload.chunks)
14
- const payload = JSON.parse(buffer.toString())
22
+ const payload = _JSONorString(buffer.toString())
15
23
  const topic = raw.source.properties.to.replace(/^topic:\/*/, '')
16
24
  await cb(topic, payload, null, { done: raw.done, failed: raw.failed })
17
25
  })
@@ -1,17 +1,15 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log('messaging')
3
3
 
4
- const MAX_NUMBER_RECONNECTS = 1000
5
4
  const MAX_WAITING_TIME = 1480000
6
5
 
7
6
  const _waitingTime = x => (x > 18 ? MAX_WAITING_TIME : (Math.pow(1.5, x) + Math.random()) * 1000)
8
7
 
9
- const _periodicallyReconnect = (client, x, n) => {
8
+ const _connectUntilConnected = (client, x) => {
10
9
  setTimeout(() => {
11
10
  connect(client, true).catch(e => {
12
- LOG._warn && LOG.warn(`Connection to Enterprise Messaging Client lost: Unsuccessful attempt to reconnect (${n}).`)
13
- /* istanbul ignore else */
14
- if (n < MAX_NUMBER_RECONNECTS) _periodicallyReconnect(client, x + 1, n + 1)
11
+ LOG._warn && LOG.warn(`Connection to Enterprise Messaging Client lost: Unsuccessful attempt to reconnect (${x}).`)
12
+ _connectUntilConnected(client, x + 1)
15
13
  })
16
14
  }, _waitingTime(x))
17
15
  }
@@ -30,22 +28,21 @@ const connect = (client, keepAlive) => {
30
28
  client.disconnect()
31
29
  })
32
30
 
31
+ if (keepAlive) {
32
+ client.once('disconnected', () => {
33
+ client.removeAllListeners('error')
34
+ client.removeAllListeners('connected')
35
+ _connectUntilConnected(client, 0)
36
+ })
37
+ }
38
+
33
39
  resolve(this)
34
40
  })
35
41
  .once('error', err => {
36
- client.removeAllListeners('disconnected')
37
42
  client.removeAllListeners('connected')
38
43
  reject(err)
39
44
  })
40
45
 
41
- if (keepAlive) {
42
- client.once('disconnected', () => {
43
- client.removeAllListeners('error')
44
- client.removeAllListeners('connected')
45
- _periodicallyReconnect(client, 0, 0)
46
- })
47
- }
48
-
49
46
  client.connect()
50
47
  })
51
48
  }
@@ -6,7 +6,7 @@ const _queueName = ({ appName, appID, ownNamespace }) => {
6
6
  const queueName = (options, optionsApp = {}) => {
7
7
  const namespace = options.credentials && options.credentials.namespace
8
8
  if (options.queue && options.queue.name) {
9
- if (options.credentials.namespace) return options.queue.name.replace(/\$namespace/g, options.credentials.namespace)
9
+ if (namespace) return options.queue.name.replace(/\$namespace/g, namespace)
10
10
  return options.queue.name
11
11
  }
12
12
  const ownNamespace = namespace
@@ -65,7 +65,8 @@ class EndpointRegistry {
65
65
  const other = authInfo
66
66
  ? {
67
67
  _: { req: { authInfo, headers: {}, query: {} } }, // for messaging to retrieve subdomain
68
- user: new cds.User.Privileged({ tenant: tenantId }) // usual tenant identification
68
+ user: new cds.User.Privileged(),
69
+ tenant: tenantId
69
70
  }
70
71
  : {}
71
72
  if (!cb) return res.sendStatus(200)
@@ -29,6 +29,20 @@ class MQManagement {
29
29
  return res.body
30
30
  }
31
31
 
32
+ async getQueues() {
33
+ const res = await authorizedRequest({
34
+ method: 'GET',
35
+ uri: this.options.url,
36
+ path: `/v1/management/queues`,
37
+ oa2: this.options.auth.oauth2,
38
+ attemptInfo: () => LOG._info && LOG.info('Get queues'),
39
+ errMsg: `Queues could not be retrieved`,
40
+ target: { kind: 'QUEUE' },
41
+ tokenStore: this
42
+ })
43
+ return res.body && res.body.results
44
+ }
45
+
32
46
  createQueue(queueName = this.queueName) {
33
47
  return authorizedRequest({
34
48
  method: 'PUT',
@@ -121,6 +135,10 @@ class MQManagement {
121
135
  }
122
136
  await Promise.all([...this.subscribedTopics].map(kv => kv[0]).map(t => this.createSubscription(t)))
123
137
  }
138
+
139
+ waitUntilReady() {
140
+ return this
141
+ }
124
142
  }
125
143
 
126
144
  class MessageQueuing extends AMQPWebhookMessaging {
@@ -12,7 +12,7 @@ if (!LOG._debug) {
12
12
  const { resolveView, getTransition, restoreLink, findQueryTarget } = require('../common/utils/resolveView')
13
13
  const { postProcess } = require('../common/utils/postProcessing')
14
14
  const { getKind, run, getDestination, getAdditionalOptions, getReqOptions } = require('./utils/client')
15
- const { formatVal } = require('../odata/cqn2odata')
15
+ const { formatVal } = require('../../odata/utils')
16
16
 
17
17
  const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
18
18
 
@@ -125,6 +125,7 @@ class RemoteService extends cds.Service {
125
125
  }
126
126
 
127
127
  this.datasource = this.options.datasource
128
+ this.destinationOptions = this.options.destinationOptions
128
129
  this.destination =
129
130
  this.options.credentials.destination ||
130
131
  getDestination((this.definition && this.definition.name) || this.datasource, this.options.credentials)
@@ -133,6 +134,11 @@ class RemoteService extends cds.Service {
133
134
  this.path = this.options.credentials.path
134
135
  this.kind = getKind(this.options) // TODO: Simplify
135
136
 
137
+ const clearKeysFromData = function (req) {
138
+ if (req.target && req.target.keys) for (const k of Object.keys(req.target.keys)) delete req.data[k]
139
+ }
140
+ this.before('UPDATE', '*', Object.assign(clearKeysFromData, { _initial: true }))
141
+
136
142
  for (const each of this.entities) {
137
143
  for (const a in each.actions) {
138
144
  _addHandlerActionFunction(this, each.actions[a], each)
@@ -153,7 +159,13 @@ class RemoteService extends cds.Service {
153
159
  const resolvedTarget = resolvedTargetOfQuery(query) || getTransition(req.target, this).target
154
160
  const reqOptions = getReqOptions(req, query, this)
155
161
  reqOptions.headers = _setHeaders(reqOptions.headers, req)
156
- const additionalOptions = getAdditionalOptions(req, this.destination, this.kind, resolvedTarget)
162
+ const additionalOptions = getAdditionalOptions(
163
+ req,
164
+ this.destination,
165
+ this.kind,
166
+ resolvedTarget,
167
+ this.destinationOptions
168
+ )
157
169
 
158
170
  // hidden compat flag in order to suppress logging response body of failed request
159
171
  if (req._suppressRemoteResponseBody) {
@@ -0,0 +1,7 @@
1
+ import sdkCore from '@sap-cloud-sdk/core'
2
+ export interface DestinationOptions extends Omit<sdkCore.DestinationOptions, 'selectionStrategy'> {
3
+ /*
4
+ * @see https://sap.github.io/cloud-sdk/api/1.50.0/modules/sap_cloud_sdk_core#DestinationSelectionStrategies
5
+ */
6
+ selectionStrategy?: 'alwaysProvider' | 'alwaysSubscriber' | 'subscriberFirst'
7
+ }
@@ -1,8 +1,8 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log('remote')
3
- const cdsLocale = require('../../../../lib/req/locale.js')
4
3
 
5
- const generateQuery = require('../cqn2odata')
4
+ const cdsLocale = require('../../../../lib/req/locale')
5
+
6
6
  const { convertV2ResponseData } = require('./dataConversion')
7
7
 
8
8
  let _cloudSdkCore
@@ -14,16 +14,38 @@ const PPPD = {
14
14
  DELETE: 1
15
15
  }
16
16
 
17
- const _executeHttpRequest = (...args) => {
18
- if (!_cloudSdkCore) _cloudSdkCore = require('@sap-cloud-sdk/core')
17
+ const KINDS_SUPPORTING_BATCH = { odata: 1, 'odata-v2': 1, 'odata-v4': 1 }
18
+
19
+ const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
20
+ const { getDestination, executeHttpRequest } = cloudSdkCore()
19
21
 
20
- const opts = args[1]
21
- if (PPPD[opts.method] && cds.env.features.fetch_csrf) {
22
- if (args.length === 3) args[2].fetchCsrfToken = true
23
- else args.push({ fetchCsrfToken: true })
22
+ const destinationName = typeof destination === 'string' && destination
23
+ if (destinationName) {
24
+ destination = await getDestination(destinationName, resolveDestinationOptions(destinationOptions, jwt))
25
+ } else if (destination.forwardAuthToken) {
26
+ destination = {
27
+ ...destination,
28
+ headers: destination.headers ? { ...destination.headers } : {},
29
+ authentication: 'NoAuthentication'
30
+ }
31
+ delete destination.forwardAuthToken
32
+ if (jwt) {
33
+ destination.headers.authorization = `Bearer ${jwt}`
34
+ } else {
35
+ LOG._warn && LOG.warn('Missing JWT token for forwardAuthToken')
36
+ }
24
37
  }
25
38
 
26
- return _cloudSdkCore.executeHttpRequest(...args)
39
+ let requestOptions
40
+ if (PPPD[requestConfig.method] && cds.env.features.fetch_csrf) {
41
+ requestOptions = { fetchCsrfToken: true }
42
+ }
43
+
44
+ return executeHttpRequest(destination, requestConfig, requestOptions)
45
+ }
46
+
47
+ const cloudSdkCore = function () {
48
+ return _cloudSdkCore || (_cloudSdkCore = require('@sap-cloud-sdk/core'))
27
49
  }
28
50
 
29
51
  const getDestination = (name, credentials) => {
@@ -34,6 +56,26 @@ const getDestination = (name, credentials) => {
34
56
  return { name, ...credentials }
35
57
  }
36
58
 
59
+ /**
60
+ * @param {import('./client-types').DestinationOptions} [options]
61
+ * @param {string} [jwt]
62
+ * @returns {import('@sap-cloud-sdk/core').DestinationOptions}
63
+ */
64
+ const resolveDestinationOptions = function (options, jwt) {
65
+ if (!options && !jwt) return undefined
66
+
67
+ const resolvedOptions = Object.assign({}, options || {})
68
+ resolvedOptions.userJwt = jwt
69
+
70
+ if (options && options.selectionStrategy) {
71
+ resolvedOptions.selectionStrategy = cloudSdkCore().DestinationSelectionStrategies[options.selectionStrategy]
72
+ if (!resolvedOptions.selectionStrategy)
73
+ throw new Error(`Unsupported destination selection strategy "${options.selectionStrategy}".`)
74
+ }
75
+
76
+ return resolvedOptions
77
+ }
78
+
37
79
  const getKind = options => {
38
80
  const kind = (options.credentials && options.credentials.kind) || options.kind
39
81
  if (typeof kind === 'object') {
@@ -175,17 +217,19 @@ const _getSanitizedError = (e, reqOptions, suppressRemoteResponseBody) => {
175
217
  return e
176
218
  }
177
219
 
178
- const run = async (reqOptions, { destination, jwt, kind, resolvedTarget, suppressRemoteResponseBody }) => {
179
- const dest = typeof destination === 'string' ? { destinationName: destination, jwt } : destination
180
-
220
+ // eslint-disable-next-line complexity
221
+ const run = async (
222
+ requestConfig,
223
+ { destination, jwt, kind, resolvedTarget, suppressRemoteResponseBody, destinationOptions }
224
+ ) => {
181
225
  let response
182
226
  try {
183
- response = await _executeHttpRequest(dest, reqOptions)
227
+ response = await _executeHttpRequest({ requestConfig, destination, destinationOptions, jwt })
184
228
  } catch (e) {
185
229
  // > axios received status >= 400 -> gateway error
186
230
  e.message = e.message ? 'Error during request to remote service: ' + e.message : 'Request to remote service failed.'
187
231
 
188
- const sanitizedError = _getSanitizedError(e, reqOptions, suppressRemoteResponseBody)
232
+ const sanitizedError = _getSanitizedError(e, requestConfig, suppressRemoteResponseBody)
189
233
 
190
234
  LOG._warn && LOG.warn(sanitizedError)
191
235
 
@@ -198,15 +242,15 @@ const run = async (reqOptions, { destination, jwt, kind, resolvedTarget, suppres
198
242
  response.headers['content-type'] &&
199
243
  response.headers['content-type'].includes('text/html') &&
200
244
  !(
201
- reqOptions.headers.accept.includes('text/html') ||
202
- reqOptions.headers.accept.includes('text/*') ||
203
- reqOptions.headers.accept.includes('*/*')
245
+ requestConfig.headers.accept.includes('text/html') ||
246
+ requestConfig.headers.accept.includes('text/*') ||
247
+ requestConfig.headers.accept.includes('*/*')
204
248
  )
205
249
  ) {
206
250
  const e = new Error("Received content-type 'text/html' which is not part of accepted content types")
207
251
  e.response = response
208
252
 
209
- const sanitizedError = _getSanitizedError(e, reqOptions, suppressRemoteResponseBody)
253
+ const sanitizedError = _getSanitizedError(e, requestConfig, suppressRemoteResponseBody)
210
254
 
211
255
  LOG._warn && LOG.warn(sanitizedError)
212
256
 
@@ -216,13 +260,38 @@ const run = async (reqOptions, { destination, jwt, kind, resolvedTarget, suppres
216
260
  })
217
261
  }
218
262
 
263
+ // get result of $batch
264
+ // does only support read requests as of now
265
+ if (requestConfig._autoBatch) {
266
+ // response data splitted by empty lines
267
+ // 1. entry contains batch id and batch headers
268
+ // 2. entry contains request status code and request headers
269
+ // 3. entry contains data or error
270
+ const responseDataSplitted = response.data.split('\r\n\r\n')
271
+ // remove closing batch id
272
+ const [content] = responseDataSplitted[2].split('\r\n')
273
+ const contentJSON = JSON.parse(content)
274
+
275
+ if (responseDataSplitted[1].startsWith('HTTP/1.1 2')) {
276
+ response.data = contentJSON
277
+ }
278
+ if (responseDataSplitted[1].startsWith('HTTP/1.1 4') || responseDataSplitted[1].startsWith('HTTP/1.1 5')) {
279
+ contentJSON.message = contentJSON.message
280
+ ? 'Error during request to remote service: ' + contentJSON.message
281
+ : 'Request to remote service failed.'
282
+ const sanitizedError = _getSanitizedError(contentJSON, requestConfig)
283
+ LOG._warn && LOG.warn(sanitizedError)
284
+ throw Object.assign(new Error(contentJSON.message), { statusCode: 502, innererror: sanitizedError })
285
+ }
286
+ }
287
+
219
288
  if (kind === 'odata-v4') return _purgeODataV4(response.data)
220
- if (kind === 'odata-v2') return _purgeODataV2(response.data, resolvedTarget, reqOptions.headers)
289
+ if (kind === 'odata-v2') return _purgeODataV2(response.data, resolvedTarget, requestConfig.headers)
221
290
  if (kind === 'odata') {
222
291
  if (typeof response.data !== 'object') return response.data
223
292
  // try to guess if we need to purge v2 or v4
224
293
  if (response.data.d) {
225
- return _purgeODataV2(response.data, resolvedTarget, reqOptions.headers)
294
+ return _purgeODataV2(response.data, resolvedTarget, requestConfig.headers)
226
295
  }
227
296
  return _purgeODataV4(response.data)
228
297
  }
@@ -241,7 +310,7 @@ const getJwt = req => {
241
310
  }
242
311
 
243
312
  const _cqnToReqOptions = (query, kind, model) => {
244
- const queryObject = generateQuery(query, kind, model)
313
+ const queryObject = cds.odata.urlify(query, { kind, model })
245
314
  return {
246
315
  method: queryObject.method,
247
316
  url: encodeURI(
@@ -312,14 +381,39 @@ const getReqOptions = (req, query, service) => {
312
381
  }
313
382
  reqOptions.url = formatPath(reqOptions.url)
314
383
 
384
+ // batch envelope if needed
385
+ if (
386
+ KINDS_SUPPORTING_BATCH[service.kind] &&
387
+ reqOptions.method === 'GET' &&
388
+ reqOptions.url.length > ((cds.env.remote && cds.env.remote.max_get_url_length) || 1028)
389
+ ) {
390
+ reqOptions._autoBatch = true
391
+ reqOptions.data = [
392
+ '--batch1',
393
+ 'Content-Type: application/http',
394
+ 'Content-Transfer-Encoding: binary',
395
+ '',
396
+ `${reqOptions.method} ${reqOptions.url.replace(/^\//, '')} HTTP/1.1`,
397
+ ...Object.keys(reqOptions.headers).map(k => `${k}: ${reqOptions.headers[k]}`),
398
+ '',
399
+ '',
400
+ '--batch1--',
401
+ ''
402
+ ].join('\r\n')
403
+ reqOptions.method = 'POST'
404
+ reqOptions.headers.accept = 'multipart/mixed'
405
+ reqOptions.headers['content-type'] = 'multipart/mixed; boundary=batch1'
406
+ reqOptions.url = '/$batch'
407
+ }
408
+
315
409
  if (service.path) reqOptions.url = `${encodeURI(service.path)}${reqOptions.url}`
316
410
 
317
411
  return reqOptions
318
412
  }
319
413
 
320
- const getAdditionalOptions = (req, destination, kind, resolvedTarget) => {
414
+ const getAdditionalOptions = (req, destination, kind, resolvedTarget, destinationOptions) => {
321
415
  const jwt = getJwt(req)
322
- const additionalOptions = { destination, kind, resolvedTarget }
416
+ const additionalOptions = { destination, kind, resolvedTarget, destinationOptions }
323
417
  if (jwt) additionalOptions.jwt = jwt
324
418
  return additionalOptions
325
419
  }
@@ -71,7 +71,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
71
71
  */
72
72
  // all others, i.e. CREATE, DROP table, ...
73
73
  this.on('*', function (req) {
74
- return this._run(this.model, this.dbc, req.query || req.event, req)
74
+ return this._run(this.model, this.dbc, req.query || req.event, req, req.data)
75
75
  })
76
76
  }
77
77
 
@@ -189,7 +189,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
189
189
  log('DROP TABLE IF EXISTS ' + entity + ';')
190
190
  }
191
191
  log()
192
- for (const each of createEntities) log(each + ';\n')
192
+ for (const each of createEntities) log(each + '\n')
193
193
  return
194
194
  }
195
195
 
@@ -1,10 +1,8 @@
1
- const { foreignKeyPropagations } = require('../common/utils/foreignKeyPropagations')
2
1
  const getError = require('../common/error')
3
2
 
4
3
  function _convertRefForAssocToOneManaged(element, refEntry) {
5
4
  const maybeManagedKey = refEntry.ref.join('_')
6
- const _foreignKeyPropagations = foreignKeyPropagations(element)
7
- if (_foreignKeyPropagations.filter(key => key.parentFieldName === maybeManagedKey)[0]) {
5
+ if (element._foreignKeys.find(key => key.parentElement && key.parentElement.name === maybeManagedKey)) {
8
6
  refEntry.ref = [maybeManagedKey]
9
7
  } else {
10
8
  throw getError(501, 'Path expressions in query options are not supported on SQLite')
@@ -0,0 +1,33 @@
1
+ const cds = require('../_runtime/cds')
2
+
3
+ const express = require('express')
4
+ const { graphqlHTTP } = require('express-graphql')
5
+ const { makeExecutableSchema } = require('@graphql-tools/schema')
6
+
7
+ const { generate } = require('./schema')
8
+ const { fieldResolver, createRootResolvers } = require('./resolvers')
9
+
10
+ class GraphQLAdapter extends express.Router {
11
+ constructor(services, options) {
12
+ super()
13
+ const mergedOptions = { ...defaultOptions, ...options }
14
+
15
+ const path = mergedOptions.path
16
+ delete mergedOptions.path
17
+
18
+ const applicationServices = Object.values(services).filter(service => service instanceof cds.ApplicationService)
19
+
20
+ const typeDefs = generate(applicationServices)
21
+ const resolvers = createRootResolvers(applicationServices)
22
+
23
+ const schema = makeExecutableSchema({ typeDefs, resolvers })
24
+
25
+ this.use(path, graphqlHTTP({ fieldResolver, schema, ...mergedOptions }))
26
+ }
27
+ }
28
+
29
+ const defaultOptions = {
30
+ path: '/graphql'
31
+ }
32
+
33
+ module.exports = GraphQLAdapter