@sap/cds 6.1.3 → 6.2.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 (206) hide show
  1. package/CHANGELOG.md +83 -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/util/assert.js +4 -0
  113. package/libx/_runtime/common/aspects/relation.js +1 -1
  114. package/libx/_runtime/common/composition/data.js +61 -15
  115. package/libx/_runtime/common/composition/delete.js +0 -1
  116. package/libx/_runtime/common/composition/insert.js +0 -1
  117. package/libx/_runtime/common/composition/tree.js +4 -10
  118. package/libx/_runtime/common/composition/update.js +44 -21
  119. package/libx/_runtime/common/generic/auth/capabilities.js +8 -10
  120. package/libx/_runtime/common/generic/crud.js +1 -2
  121. package/libx/_runtime/common/generic/etag.js +4 -4
  122. package/libx/_runtime/common/generic/input.js +4 -4
  123. package/libx/_runtime/common/generic/paging.js +3 -3
  124. package/libx/_runtime/common/generic/put.js +3 -3
  125. package/libx/_runtime/common/generic/sorting.js +4 -4
  126. package/libx/_runtime/common/generic/temporal.js +3 -3
  127. package/libx/_runtime/common/i18n/messages.properties +0 -7
  128. package/libx/_runtime/common/utils/cqn2cqn4sql.js +11 -6
  129. package/libx/_runtime/common/utils/csn.js +0 -28
  130. package/libx/_runtime/common/utils/draft.js +8 -1
  131. package/libx/_runtime/common/utils/path.js +7 -1
  132. package/libx/_runtime/common/utils/resolveView.js +2 -3
  133. package/libx/_runtime/db/data-conversion/post-processing.js +3 -44
  134. package/libx/_runtime/db/generic/input.js +3 -3
  135. package/libx/_runtime/db/sql-builder/dataTypes.js +4 -0
  136. package/libx/_runtime/fiori/generic/activate.js +2 -2
  137. package/libx/_runtime/fiori/generic/before.js +40 -72
  138. package/libx/_runtime/fiori/generic/cancel.js +2 -2
  139. package/libx/_runtime/fiori/generic/delete.js +2 -2
  140. package/libx/_runtime/fiori/generic/edit.js +2 -2
  141. package/libx/_runtime/fiori/generic/new.js +2 -2
  142. package/libx/_runtime/fiori/generic/patch.js +49 -37
  143. package/libx/_runtime/fiori/generic/prepare.js +2 -2
  144. package/libx/_runtime/fiori/generic/read.js +27 -37
  145. package/libx/_runtime/fiori/utils/where.js +4 -2
  146. package/libx/_runtime/hana/Service.js +1 -3
  147. package/libx/_runtime/hana/conversion.js +3 -0
  148. package/libx/_runtime/hana/driver.js +33 -3
  149. package/libx/_runtime/hana/dynatrace.js +1 -0
  150. package/libx/_runtime/hana/search2Contains.js +12 -1
  151. package/libx/_runtime/hana/search2cqn4sql.js +10 -27
  152. package/libx/_runtime/hana/streaming.js +1 -0
  153. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  154. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -0
  155. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +5 -2
  156. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -0
  157. package/libx/_runtime/messaging/enterprise-messaging.js +62 -3
  158. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  159. package/libx/_runtime/messaging/redis-messaging.js +1 -0
  160. package/libx/_runtime/remote/Service.js +2 -2
  161. package/libx/_runtime/remote/utils/client.js +8 -3
  162. package/libx/_runtime/remote/utils/data.js +7 -2
  163. package/libx/_runtime/sqlite/Service.js +18 -7
  164. package/libx/_runtime/sqlite/conversion.js +3 -0
  165. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +3 -3
  166. package/libx/_runtime/sqlite/localized.js +8 -8
  167. package/libx/odata/afterburner.js +39 -7
  168. package/libx/odata/cqn2odata.js +6 -3
  169. package/libx/odata/grammar.pegjs +66 -18
  170. package/libx/odata/index.js +3 -2
  171. package/libx/odata/parser.js +1 -1
  172. package/libx/odata/utils.js +2 -0
  173. package/libx/rest/RestAdapter.js +62 -43
  174. package/libx/rest/middleware/parse.js +2 -1
  175. package/libx/rest/middleware/update.js +1 -1
  176. package/package.json +2 -2
  177. package/server.js +5 -4
  178. package/srv/mtx.cds +1 -1
  179. package/srv/mtx.js +4 -33
  180. package/lib/srv/adapters.js +0 -85
  181. package/lib/utils/resources/index.js +0 -48
  182. package/lib/utils/resources/tar.js +0 -49
  183. package/lib/utils/resources/utils.js +0 -11
  184. package/libx/_runtime/extensibility/activate.js +0 -69
  185. package/libx/_runtime/extensibility/add.js +0 -50
  186. package/libx/_runtime/extensibility/addExtension.js +0 -72
  187. package/libx/_runtime/extensibility/defaults.js +0 -34
  188. package/libx/_runtime/extensibility/handler/transformREAD.js +0 -121
  189. package/libx/_runtime/extensibility/handler/transformRESULT.js +0 -51
  190. package/libx/_runtime/extensibility/handler/transformWRITE.js +0 -64
  191. package/libx/_runtime/extensibility/linter/allowlist_checker.js +0 -373
  192. package/libx/_runtime/extensibility/linter/annotations_checker.js +0 -113
  193. package/libx/_runtime/extensibility/linter/checker_base.js +0 -20
  194. package/libx/_runtime/extensibility/linter/namespace_checker.js +0 -180
  195. package/libx/_runtime/extensibility/linter.js +0 -32
  196. package/libx/_runtime/extensibility/push.js +0 -118
  197. package/libx/_runtime/extensibility/service.js +0 -38
  198. package/libx/_runtime/extensibility/token.js +0 -57
  199. package/libx/_runtime/extensibility/utils.js +0 -131
  200. package/libx/_runtime/extensibility/validation.js +0 -50
  201. package/libx/_runtime/extensibility/views.js +0 -12
  202. package/srv/extensibility-service.cds +0 -60
  203. package/srv/extensibility-service.js +0 -1
  204. package/srv/extensions.cds +0 -8
  205. package/srv/model-provider.cds +0 -61
  206. package/srv/model-provider.js +0 -143
@@ -1,40 +1,25 @@
1
1
  const cds = require('../../cds')
2
2
  const { UPDATE, SELECT } = cds.ql
3
3
 
4
- const { getUpdateDraftAdminCQN, ensureDraftsSuffix, ensureNoDraftsSuffix, addColumnAlias } = require('../utils/handler')
5
- const { getKeysCondition } = require('../utils/where')
6
- const { getColumns } = require('../../cds-services/services/utils/columns')
7
- const { DRAFT_COLUMNS_CQN } = require('../../common/constants/draft')
8
-
9
- const _getSelectCQN = (model, { target: { name } }, keysCondition, checkUser = true) => {
10
- const activeName = ensureNoDraftsSuffix(name)
11
- const draftName = ensureDraftsSuffix(name)
12
-
13
- const columns = [
14
- ...addColumnAlias(
15
- getColumns(model.definitions[activeName], { removeIgnore: true, filterVirtual: true }).map(obj => obj.name),
16
- draftName
17
- ),
18
- ...DRAFT_COLUMNS_CQN
19
- ]
20
-
21
- if (checkUser) {
22
- columns.push({ ref: ['DRAFT.DraftAdministrativeData', 'inProcessByUser'], as: 'draftAdmin_inProcessByUser' })
4
+ const { getUpdateDraftAdminCQN, ensureDraftsSuffix } = require('../utils/handler')
5
+ const { removeIsActiveEntityRecursively } = require('../utils/where')
6
+ const { deepCopyArray } = require('../../common/utils/copy')
7
+ const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
8
+
9
+ const _getSelectCQN = ({ query }) => {
10
+ const fromRef = deepCopyArray(query.UPDATE.entity.ref)
11
+ for (const item of fromRef) {
12
+ if (item.where) {
13
+ item.where = removeIsActiveEntityRecursively(item.where)
14
+ }
23
15
  }
16
+ fromRef[0].id = ensureDraftsSuffix(fromRef[0].id)
17
+ const from = { ref: fromRef }
24
18
 
25
- // REVISIT: support navigation to one
26
- return SELECT.one(draftName)
27
- .columns(columns)
28
- .join('DRAFT.DraftAdministrativeData')
29
- .on([
30
- { ref: [draftName, 'DraftAdministrativeData_DraftUUID'] },
31
- '=',
32
- { ref: ['DRAFT.DraftAdministrativeData', 'DraftUUID'] }
33
- ])
34
- .where(keysCondition)
19
+ return SELECT.from(from)
35
20
  }
36
21
 
37
- const _getUpdateDraftCQN = ({ query, target: { name } }, keysCondition) => {
22
+ const _getUpdateDraftCQN = ({ query }, where, targetRef) => {
38
23
  const set = {}
39
24
 
40
25
  for (const entry in query.UPDATE.data) {
@@ -46,7 +31,23 @@ const _getUpdateDraftCQN = ({ query, target: { name } }, keysCondition) => {
46
31
  }
47
32
 
48
33
  if (set.IsActiveEntity) set.IsActiveEntity = false
49
- return UPDATE(ensureDraftsSuffix(name)).data(set).where(keysCondition)
34
+
35
+ return UPDATE(targetRef).data(set).where(where)
36
+ }
37
+
38
+ const _joinDraftAdministrativeData = (selectResolved, target) => {
39
+ const columns = []
40
+ columns.push({ ref: ['DRAFT.DraftAdministrativeData', 'inProcessByUser'], as: 'draftAdmin_inProcessByUser' })
41
+ columns.push({ ref: [target, 'DraftAdministrativeData_DraftUUID'] })
42
+
43
+ return selectResolved
44
+ .columns(columns)
45
+ .join('DRAFT.DraftAdministrativeData')
46
+ .on([
47
+ { ref: [target, 'DraftAdministrativeData_DraftUUID'] },
48
+ '=',
49
+ { ref: ['DRAFT.DraftAdministrativeData', 'DraftUUID'] }
50
+ ])
50
51
  }
51
52
 
52
53
  /**
@@ -57,20 +58,31 @@ const _getUpdateDraftCQN = ({ query, target: { name } }, keysCondition) => {
57
58
  *
58
59
  * @param req
59
60
  */
60
- const _handler = async function (req) {
61
+ const fioriGenericPatch = async function (req) {
61
62
  if (req.data.IsActiveEntity === true) req.reject(400, 'Patch can only be applied to a draft entity')
62
63
 
63
- const keysCondition = getKeysCondition(req.target, req.data)
64
64
  const dbtx = cds.tx(req)
65
- let result = await dbtx.run(_getSelectCQN(this.model, req, keysCondition))
65
+
66
+ const selectResolved = cqn2cqn4sql(_getSelectCQN(req), this.model)
67
+
68
+ const targetName = selectResolved.SELECT.from.ref[selectResolved.SELECT.from.ref.length - 1]
69
+ const alias = selectResolved.SELECT.from.as
70
+ const selectWithAdmin = _joinDraftAdministrativeData(selectResolved, alias || targetName)
71
+ const results = await dbtx.run(selectWithAdmin)
72
+
73
+ if (results.length === 0) req.reject(404)
74
+
75
+ const result = results[0]
66
76
 
67
77
  // Potential timeout scenario supported
68
78
  if (result.draftAdmin_inProcessByUser && result.draftAdmin_inProcessByUser !== req.user.id) {
69
79
  // REVISIT: security log?
70
80
  req.reject(403)
71
81
  }
72
-
73
- const updateDraftCQN = _getUpdateDraftCQN(req, keysCondition)
82
+ const updateDraftCQN = _getUpdateDraftCQN(req, selectResolved.SELECT.where, {
83
+ ref: [targetName],
84
+ as: alias || targetName
85
+ })
74
86
  const updateDraftAdminCQN = getUpdateDraftAdminCQN(req, result.DraftAdministrativeData_DraftUUID)
75
87
 
76
88
  await Promise.all([dbtx.run(updateDraftCQN), dbtx.run(updateDraftAdminCQN)])
@@ -80,5 +92,5 @@ const _handler = async function (req) {
80
92
  }
81
93
 
82
94
  module.exports = cds.service.impl(function (srv, entity) {
83
- srv.on('PATCH', entity, _handler)
95
+ srv.on('PATCH', entity, fioriGenericPatch)
84
96
  })
@@ -11,7 +11,7 @@ const { getColumns } = require('../../cds-services/services/utils/columns')
11
11
  *
12
12
  * @param req
13
13
  */
14
- const _handler = async function (req) {
14
+ const fioriGenericPrepare = async function (req) {
15
15
  if (req.query.SELECT.from.ref.length > 1 || isActiveEntityRequested(req.query.SELECT.from.ref[0].where || [])) {
16
16
  req.reject(400, 'Action "draftPrepare" can only be called on a draft entity')
17
17
  }
@@ -44,5 +44,5 @@ const _handler = async function (req) {
44
44
  }
45
45
 
46
46
  module.exports = cds.service.impl(function (srv, entity) {
47
- srv.on('draftPrepare', entity, _handler)
47
+ srv.on('draftPrepare', entity, fioriGenericPrepare)
48
48
  })
@@ -4,6 +4,7 @@ const { SELECT } = cds.ql
4
4
  const { cqn2cqn4sql, convertWhereExists } = require('../../common/utils/cqn2cqn4sql')
5
5
  const { getElementDeep } = require('../../common/utils/csn')
6
6
  const { DRAFT_COLUMNS_MAP, SCENARIO } = require('../../common/constants/draft')
7
+ const { filterNonDraftColumns } = require('../../common/utils/draft')
7
8
  const {
8
9
  addColumnAlias,
9
10
  draftIsLocked,
@@ -16,15 +17,11 @@ const {
16
17
  filterKeys
17
18
  } = require('../utils/handler')
18
19
  const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively } = require('../utils/where')
19
- const { getColumns } = require('../../cds-services/services/utils/columns')
20
20
  const { adaptStreamCQN } = require('../../cds-services/adapter/odata-v4/utils/stream')
21
21
 
22
22
  const _findSubselect = where => {
23
23
  return where.find((e, i) => {
24
- if (e.xpr) {
25
- return _findSubselect(e.xpr)
26
- }
27
-
24
+ if (e.xpr) return _findSubselect(e.xpr)
28
25
  return e.SELECT && where[i - 1] === 'exists'
29
26
  })
30
27
  }
@@ -626,6 +623,9 @@ const _siblingEntity = (
626
623
 
627
624
  if (siblingIndex === 0) {
628
625
  const columnCqnPartial = columns.map(col => {
626
+ if (col.val) {
627
+ return Object.assign({}, col)
628
+ }
629
629
  const colName = col.ref ? col.ref[col.ref.length - 1] : col
630
630
  const ref = col.ref ? [table.as, ...col.ref] : [table.as, colName]
631
631
  return Object.assign({}, col, { ref })
@@ -694,7 +694,7 @@ function _siblingSubScenario(nav, siblingIndex, siblingQuery, target, params, mo
694
694
  const existsIdx = siblingQuery.SELECT.where.indexOf('exists')
695
695
  if (existsIdx > -1) subReq.query.where(siblingQuery.SELECT.where.slice(existsIdx, existsIdx + 2))
696
696
  const subOrigFrom = { ref: [...subNav].reverse() }
697
- subScenario = _generateCQN(subOrigFrom, subReq, [{ val: 1 }], model)
697
+ subScenario = _generateCQN(subReq, [{ val: 1 }], subOrigFrom, model)
698
698
  subScenario.cqn.where(onCond)
699
699
  }
700
700
 
@@ -757,9 +757,8 @@ const _replaceWhereExists = (query, _siblingIndex, siblingCQN) => {
757
757
  }
758
758
  }
759
759
 
760
- const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
761
- return _replaceWhereExists(cqn, siblingIndex, siblingCQN)
762
- }
760
+ const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) =>
761
+ _replaceWhereExists(cqn, siblingIndex, siblingCQN)
763
762
 
764
763
  const _getDraftDoc = (req, draftName, draftWhere) => {
765
764
  const refDraft = req.query.SELECT.from.as ? { ref: [draftName], as: req.query.SELECT.from.as } : draftName
@@ -784,8 +783,10 @@ const _getDraftDoc = (req, draftName, draftWhere) => {
784
783
 
785
784
  const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
786
785
  const enrichedCol = []
786
+
787
787
  if (orderBy && orderBy.length > 1) {
788
788
  const colNames = columns.map(el => el.ref[el.ref.length - 1])
789
+
789
790
  // REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
790
791
  for (const el of orderBy) {
791
792
  // For associations we need to 'materialise' the resulting field, otherwise we cannot access it in an outer SELECT.
@@ -906,11 +907,11 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model, ent
906
907
  }
907
908
 
908
909
  return union
909
- .columns(...columns)
910
+ .columns(...columns.map(ref => (ref.as ? { ref: [ref.as], as: ref.as } : ref))) // needed for aliased stream property ref@odata.mediaContentType
910
911
  .columns(..._filterDraftColumnsBySelected(DRAFT_COLUMNS_CASTED, req.query.SELECT.columns))
911
912
  }
912
913
 
913
- const _excludeActiveDraftExists = (req, draftWhere, columns, model) => {
914
+ const _excludeActiveDraftExists = (req, columns, draftWhere, model) => {
914
915
  const { table, name } = _getTableName(req, true)
915
916
  const draftName = table.ref[0]
916
917
 
@@ -964,8 +965,9 @@ const _validatedWithSiblingInProcess = (req, draftWhere, draftParameters, column
964
965
  if (
965
966
  !draftInProcessByUser &&
966
967
  _isValidExcludeActiveDraftExists(draftParameters.isActiveEntity, draftParameters.siblingIsActive)
967
- )
968
- return _excludeActiveDraftExists(req, draftWhere, columns, model)
968
+ ) {
969
+ return _excludeActiveDraftExists(req, columns, draftWhere, model)
970
+ }
969
971
 
970
972
  if (
971
973
  draftInProcessByUser &&
@@ -1005,8 +1007,8 @@ const _draftInSubSelect = (where, req) => {
1005
1007
  const _isDraftAdminScenario = req =>
1006
1008
  req.target.query && req.target.query._target && req.target.query._target.name === 'DRAFT.DraftAdministrativeData'
1007
1009
 
1008
- const _generateCQN = (originalFrom, req, columns, model) => {
1009
- const nav = [...originalFrom.ref].reverse() || []
1010
+ const _generateCQN = (req, columns, from, model) => {
1011
+ const nav = [...from.ref].reverse() || []
1010
1012
  let siblingIndex = nav.indexOf('SiblingEntity')
1011
1013
 
1012
1014
  // it can also be a property access (new parser), then we must shift it
@@ -1018,20 +1020,12 @@ const _generateCQN = (originalFrom, req, columns, model) => {
1018
1020
  let siblingScenario
1019
1021
  if (siblingIndex > -1) {
1020
1022
  siblingScenario = _getSiblingScenario(req, columns, model, siblingIndex, nav)
1021
- if (siblingIndex === 0) {
1022
- return siblingScenario
1023
- }
1024
-
1023
+ if (siblingIndex === 0) return siblingScenario
1025
1024
  _mergeSiblingIntoCQN(req.query, siblingScenario, siblingIndex - 1)
1026
1025
  }
1027
1026
 
1028
- if (_isDraftAdminScenario(req)) {
1029
- return _draftAdminTable(req)
1030
- }
1031
-
1032
- if (!req.query.SELECT.where) {
1033
- return _allActive(req, columns)
1034
- }
1027
+ if (_isDraftAdminScenario(req)) return _draftAdminTable(req)
1028
+ if (!req.query.SELECT.where) return _allActive(req, columns)
1035
1029
 
1036
1030
  // REVISIT this function does not only read, but modifies where!
1037
1031
  const draftParameters = _readDraftParameters(req.query.SELECT.where)
@@ -1065,11 +1059,6 @@ const _generateCQN = (originalFrom, req, columns, model) => {
1065
1059
  return _validatedDraftOfWhichIAmOwner(req, req.query.SELECT.where, draftParameters, columns)
1066
1060
  }
1067
1061
 
1068
- const _getColumns = ({ query: { SELECT } }) =>
1069
- SELECT.columns.filter(
1070
- col => (col.ref && !(col.ref[col.ref.length - 1] in DRAFT_COLUMNS_MAP)) || (!col.ref && !(col in DRAFT_COLUMNS_MAP))
1071
- )
1072
-
1073
1062
  const _isIsActiveEntity = element => element.ref && element.ref[element.ref.length - 1] === 'IsActiveEntity'
1074
1063
 
1075
1064
  const _adaptSubSelects = ({ SELECT: { from, where } }, scenario) => {
@@ -1182,6 +1171,7 @@ const _setLastSubQuery = (query, last, prev = query) => {
1182
1171
  const _adaptDraftAdminExpand = cqn => {
1183
1172
  const draftAdminExpand =
1184
1173
  cqn.SELECT.columns && cqn.SELECT.columns.find(c => c.expand && c.ref[0] === 'DraftAdministrativeData')
1174
+
1185
1175
  if (draftAdminExpand) {
1186
1176
  _ensureDraftAdminColumnsForCalculation(draftAdminExpand.expand)
1187
1177
  }
@@ -1274,7 +1264,7 @@ const _adaptColumns4readAfterWrite = (req, cqnScenario, query4sql) => {
1274
1264
  *
1275
1265
  * @param req
1276
1266
  */
1277
- const _handler = async function (req) {
1267
+ const fioriGenericRead = async function (req) {
1278
1268
  const query = req.query
1279
1269
  const originalFrom = _copyCQNPartial(query.SELECT.from)
1280
1270
 
@@ -1299,12 +1289,12 @@ const _handler = async function (req) {
1299
1289
  // just to make existing tests working with new parser. not really tested, not needed to be supported
1300
1290
  if (reqClone.query.SELECT.from.SELECT) {
1301
1291
  const subQueryReq = { __proto__: req, query: _copyCQNPartial(_getLastSubQuery(reqClone.query)) }
1302
- const columns = _getColumns(subQueryReq)
1303
- cqnScenario = _generateCQN(originalFrom.SELECT.from, subQueryReq, columns, this.model)
1292
+ const nonDraftColumns = filterNonDraftColumns(subQueryReq.query.SELECT.columns)
1293
+ cqnScenario = _generateCQN(subQueryReq, nonDraftColumns, originalFrom.SELECT.from, this.model)
1304
1294
  cqnScenario.cqn = _setLastSubQuery(reqClone.query, cqnScenario.cqn)
1305
1295
  } else {
1306
- const columns = _getColumns(reqClone)
1307
- cqnScenario = _generateCQN(originalFrom, reqClone, columns, this.model)
1296
+ const nonDraftColumns = filterNonDraftColumns(reqClone.query.SELECT.columns)
1297
+ cqnScenario = _generateCQN(reqClone, nonDraftColumns, originalFrom, this.model)
1308
1298
  }
1309
1299
 
1310
1300
  if (!cqnScenario) req.reject(400)
@@ -1334,5 +1324,5 @@ const _handler = async function (req) {
1334
1324
  }
1335
1325
 
1336
1326
  module.exports = cds.service.impl(function (srv, entity) {
1337
- srv.on('READ', entity, _handler)
1327
+ srv.on('READ', entity, fioriGenericRead)
1338
1328
  })
@@ -210,11 +210,13 @@ const extractKeyConditions = whereCondition => {
210
210
  return result
211
211
  }
212
212
 
213
- const getKeysCondition = (target, data) => {
213
+ const getKeysCondition = req => {
214
+ const data = req.data
215
+ const target = req.target
214
216
  const where = []
215
217
  for (const k in target.keys) {
216
218
  const key = target.keys[k]
217
- if (!key.isAssociation && key.name !== 'IsActiveEntity') {
219
+ if (!key.isAssociation && key.name !== 'IsActiveEntity' && data[key.name] != undefined) {
218
220
  if (where.length) where.push('and')
219
221
  where.push({ ref: [key.name] }, '=', { val: data[key.name] })
220
222
  }
@@ -163,10 +163,8 @@ class HanaDatabase extends DatabaseService {
163
163
  }
164
164
 
165
165
  // REVISIT: should happen automatically after a configurable time
166
- // poolOnly: private param for mtx
167
- async disconnect(tenant, poolOnly) {
166
+ async disconnect(tenant) {
168
167
  await pool.drain(tenant)
169
- if (!poolOnly) super.disconnect(tenant)
170
168
  }
171
169
  }
172
170
 
@@ -55,6 +55,7 @@ const convertToString = element => {
55
55
  const HANA_TYPE_CONVERSION_MAP = new Map([
56
56
  ['cds.Boolean', convertToBoolean],
57
57
  ['cds.Integer64', convertInt64ToString],
58
+ ['cds.Int64', convertInt64ToString],
58
59
  ['cds.DateTime', convertToISONoMillis],
59
60
  ['cds.Timestamp', convertToISO],
60
61
  ['cds.LargeString', convertToString],
@@ -62,10 +63,12 @@ const HANA_TYPE_CONVERSION_MAP = new Map([
62
63
  ])
63
64
 
64
65
  if (cds.env.features.bigjs) {
66
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
65
67
  const Big = require('big.js')
66
68
  const convertToBig = value => new Big(value)
67
69
 
68
70
  HANA_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
71
+ HANA_TYPE_CONVERSION_MAP.set('cds.Int64', convertToBig)
69
72
  HANA_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
70
73
  }
71
74
 
@@ -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.