@sap/cds 6.8.3 → 7.0.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 (214) hide show
  1. package/CHANGELOG.md +61 -2
  2. package/README.md +0 -1
  3. package/bin/cds-serve.js +50 -3
  4. package/bin/deploy/to-hana.js +1 -0
  5. package/bin/serve.js +16 -20
  6. package/lib/auth/basic-auth.js +6 -4
  7. package/lib/auth/index.js +4 -3
  8. package/lib/auth/jwt-auth.js +2 -5
  9. package/lib/compile/cds-compile.js +34 -89
  10. package/lib/compile/cdsc.js +11 -0
  11. package/lib/compile/etc/properties.js +2 -2
  12. package/lib/compile/for/lean_drafts.js +36 -69
  13. package/lib/compile/for/nodejs.js +2 -1
  14. package/lib/compile/load.js +1 -1
  15. package/lib/compile/minify.js +2 -0
  16. package/lib/compile/to/csn.js +74 -0
  17. package/{bin/build/provider/hana/2tabledata.js → lib/compile/to/hdbtabledata.js} +4 -6
  18. package/lib/compile/to/json.js +1 -1
  19. package/lib/compile/to/sql.js +8 -6
  20. package/lib/dbs/cds-deploy.js +174 -114
  21. package/lib/env/cds-env.js +64 -79
  22. package/lib/env/cds-requires.js +11 -28
  23. package/lib/env/defaults.js +13 -3
  24. package/lib/env/plugins.js +1 -12
  25. package/lib/env/presets.js +25 -21
  26. package/lib/index.js +121 -147
  27. package/lib/{core/reflect.js → linked/models.js} +2 -2
  28. package/lib/{core/infer.js → linked/queries.js} +2 -0
  29. package/lib/{core/index.js → linked/types.js} +2 -1
  30. package/lib/log/cds-error.js +13 -7
  31. package/lib/log/format/cf.js +1 -1
  32. package/lib/plugins.js +49 -0
  33. package/lib/ql/Query.js +0 -9
  34. package/lib/ql/STREAM.js +0 -1
  35. package/lib/req/context.js +2 -7
  36. package/lib/req/request.js +6 -2
  37. package/lib/req/response.js +23 -10
  38. package/lib/srv/middlewares/ctx-model.js +1 -1
  39. package/lib/srv/middlewares/errors.js +1 -1
  40. package/lib/srv/protocols/_legacy.js +1 -0
  41. package/lib/srv/protocols/graphql.js +7 -16
  42. package/lib/srv/protocols/index.js +59 -45
  43. package/lib/srv/protocols/odata-v2-proxy.js +2 -70
  44. package/lib/srv/srv-api.js +9 -3
  45. package/lib/srv/srv-dispatch.js +12 -9
  46. package/lib/srv/srv-models.js +4 -21
  47. package/lib/srv/srv-tx.js +15 -12
  48. package/lib/utils/cds-test.js +14 -9
  49. package/lib/utils/cds-utils.js +2 -12
  50. package/lib/utils/check-version.js +17 -0
  51. package/{bin/build → lib/utils}/csv-reader.js +23 -24
  52. package/libx/_runtime/auth/index.js +27 -23
  53. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +15 -72
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +0 -2
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +33 -63
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +14 -18
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +15 -5
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +37 -40
  61. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +7 -1
  62. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +101 -38
  63. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/AbstractError.js +5 -1
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +2 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +9 -8
  66. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +15 -11
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +4 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +5 -2
  70. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -123
  71. package/libx/_runtime/cds-services/services/Service.js +79 -107
  72. package/libx/_runtime/cds-services/services/utils/columns.js +23 -19
  73. package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -1
  74. package/libx/_runtime/cds-services/services/utils/differ.js +7 -2
  75. package/libx/_runtime/cds-services/util/assert.js +65 -2
  76. package/libx/_runtime/common/composition/data.js +1 -0
  77. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  78. package/libx/_runtime/common/generic/auth/restrict.js +5 -10
  79. package/libx/_runtime/common/generic/auth/restrictions.js +40 -0
  80. package/libx/_runtime/common/generic/auth/utils.js +1 -2
  81. package/libx/_runtime/common/generic/crud.js +32 -16
  82. package/libx/_runtime/common/generic/etag.js +133 -104
  83. package/libx/_runtime/common/generic/input.js +6 -21
  84. package/libx/_runtime/common/generic/put.js +1 -1
  85. package/libx/_runtime/common/generic/stream.js +52 -0
  86. package/libx/_runtime/common/generic/temporal.js +25 -8
  87. package/libx/_runtime/common/i18n/messages.properties +0 -2
  88. package/libx/_runtime/common/utils/cqn.js +1 -1
  89. package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -2
  90. package/libx/_runtime/common/utils/csn.js +0 -51
  91. package/libx/_runtime/common/utils/etag.js +30 -0
  92. package/libx/_runtime/common/utils/keys.js +1 -1
  93. package/libx/_runtime/common/utils/normalizeTimestamp.js +25 -0
  94. package/libx/_runtime/common/utils/path.js +1 -1
  95. package/libx/_runtime/common/utils/resolveView.js +2 -1
  96. package/libx/_runtime/common/utils/rewriteAsterisks.js +6 -4
  97. package/libx/_runtime/common/utils/search2cqn4sql.js +12 -16
  98. package/libx/_runtime/common/utils/stream.js +140 -0
  99. package/libx/_runtime/common/utils/streamProp.js +29 -12
  100. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +0 -2
  101. package/libx/_runtime/db/generic/index.js +0 -2
  102. package/libx/_runtime/db/query/delete.js +2 -2
  103. package/libx/_runtime/db/query/insert.js +2 -2
  104. package/libx/_runtime/db/query/read.js +2 -2
  105. package/libx/_runtime/db/query/run.js +2 -2
  106. package/libx/_runtime/db/query/update.js +2 -2
  107. package/libx/_runtime/db/sql-builder/BaseBuilder.js +0 -6
  108. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +23 -12
  109. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +18 -6
  110. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -0
  111. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -7
  112. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +1 -0
  113. package/libx/_runtime/db/utils/normalizeTimeData.js +7 -3
  114. package/libx/_runtime/fiori/draft.js +2 -0
  115. package/libx/_runtime/fiori/generic/activate.js +8 -9
  116. package/libx/_runtime/fiori/generic/before.js +30 -20
  117. package/libx/_runtime/fiori/generic/cancel.js +5 -3
  118. package/libx/_runtime/fiori/generic/delete.js +5 -3
  119. package/libx/_runtime/fiori/generic/edit.js +7 -7
  120. package/libx/_runtime/fiori/generic/index.js +10 -16
  121. package/libx/_runtime/fiori/generic/new.js +5 -3
  122. package/libx/_runtime/fiori/generic/patch.js +11 -8
  123. package/libx/_runtime/fiori/generic/prepare.js +13 -6
  124. package/libx/_runtime/fiori/generic/read.js +12 -6
  125. package/libx/_runtime/fiori/lean-draft.js +207 -152
  126. package/libx/_runtime/fiori/utils/delete.js +10 -5
  127. package/libx/_runtime/fiori/utils/req.js +17 -5
  128. package/libx/_runtime/fiori/utils/stream.js +36 -0
  129. package/libx/_runtime/hana/Service.js +12 -9
  130. package/libx/_runtime/hana/conversion.js +10 -15
  131. package/libx/_runtime/hana/driver.js +2 -0
  132. package/libx/_runtime/hana/execute.js +28 -6
  133. package/libx/_runtime/hana/pool.js +36 -122
  134. package/libx/_runtime/hana/search2cqn4sql.js +34 -36
  135. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -6
  136. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -1
  137. package/libx/_runtime/messaging/enterprise-messaging.js +10 -58
  138. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  139. package/libx/_runtime/remote/Service.js +20 -1
  140. package/libx/_runtime/remote/utils/client.js +3 -5
  141. package/libx/_runtime/sqlite/Service.js +4 -6
  142. package/libx/_runtime/sqlite/conversion.js +3 -13
  143. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +9 -6
  144. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +6 -1
  145. package/libx/_runtime/sqlite/execute.js +5 -16
  146. package/libx/odata/afterburner.js +22 -6
  147. package/libx/odata/grammar.pegjs +6 -1
  148. package/libx/odata/parser.js +1 -1
  149. package/libx/rest/RestAdapter.js +16 -9
  150. package/libx/rest/RestRequest.js +1 -1
  151. package/libx/rest/middleware/input.js +2 -1
  152. package/libx/rest/middleware/operation.js +1 -0
  153. package/libx/rest/middleware/parse.js +3 -2
  154. package/libx/rest/middleware/payload.js +9 -8
  155. package/libx/rest/middleware/read.js +1 -0
  156. package/package.json +9 -16
  157. package/app/fiori/preview.js +0 -270
  158. package/app/fiori/routes.js +0 -59
  159. package/bin/build/buildTaskEngine.js +0 -360
  160. package/bin/build/buildTaskFactory.js +0 -283
  161. package/bin/build/buildTaskHandler.js +0 -241
  162. package/bin/build/buildTaskProvider.js +0 -22
  163. package/bin/build/buildTaskProviderFactory.js +0 -175
  164. package/bin/build/cds.js +0 -5
  165. package/bin/build/constants.js +0 -66
  166. package/bin/build/index.js +0 -58
  167. package/bin/build/provider/buildTaskHandlerEdmx.js +0 -82
  168. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +0 -131
  169. package/bin/build/provider/buildTaskHandlerInternal.js +0 -254
  170. package/bin/build/provider/buildTaskProviderInternal.js +0 -383
  171. package/bin/build/provider/fiori/index.js +0 -171
  172. package/bin/build/provider/hana/2migration.js +0 -179
  173. package/bin/build/provider/hana/index.js +0 -505
  174. package/bin/build/provider/hana/migrationtable.js +0 -472
  175. package/bin/build/provider/hana/template/.hdiconfig-haas +0 -163
  176. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +0 -137
  177. package/bin/build/provider/hana/template/.hdinamespace +0 -4
  178. package/bin/build/provider/hana/template/package.json +0 -12
  179. package/bin/build/provider/hana/template/undeploy.json +0 -5
  180. package/bin/build/provider/java/index.js +0 -111
  181. package/bin/build/provider/java-cf/index.js +0 -1
  182. package/bin/build/provider/mtx/index.js +0 -268
  183. package/bin/build/provider/mtx/resourcesTarBuilder.js +0 -95
  184. package/bin/build/provider/mtx-extension/index.js +0 -131
  185. package/bin/build/provider/mtx-sidecar/index.js +0 -137
  186. package/bin/build/provider/node-cf/index.js +0 -1
  187. package/bin/build/provider/nodejs/index.js +0 -192
  188. package/bin/build/util.js +0 -299
  189. package/bin/cds.js +0 -125
  190. package/bin/deploy/to-hana/cfUtil.js +0 -355
  191. package/bin/deploy/to-hana/gitUtil.js +0 -57
  192. package/bin/deploy/to-hana/hana.js +0 -306
  193. package/bin/deploy/to-hana/hdiDeployUtil.js +0 -153
  194. package/bin/deploy/to-hana/index.js +0 -16
  195. package/bin/deploy/to-hana/mtaUtil.js +0 -170
  196. package/bin/mtx/in-cds.js +0 -17
  197. package/bin/plugins.js +0 -32
  198. package/bin/run.js +0 -24
  199. package/bin/utils/log.js +0 -24
  200. package/bin/version.js +0 -178
  201. package/libx/_runtime/audit/Service.js +0 -222
  202. package/libx/_runtime/audit/generic/personal/access.js +0 -61
  203. package/libx/_runtime/audit/generic/personal/index.js +0 -56
  204. package/libx/_runtime/audit/generic/personal/modification.js +0 -132
  205. package/libx/_runtime/audit/generic/personal/utils.js +0 -186
  206. package/libx/_runtime/audit/utils/log.js +0 -23
  207. package/libx/_runtime/audit/utils/v2.js +0 -176
  208. package/libx/_runtime/db/data-conversion/timestamp.js +0 -9
  209. package/libx/_runtime/db/generic/integrity.js +0 -455
  210. package/srv/audit-log.cds +0 -87
  211. package/srv/mtx.cds +0 -2
  212. package/srv/mtx.js +0 -8
  213. /package/lib/{core → linked}/classes.js +0 -0
  214. /package/lib/{core → linked}/entities.js +0 -0
@@ -29,7 +29,7 @@ const _validate = (activeResult, draftResult, req, IsActiveEntity) => {
29
29
  (IsActiveEntity === true && activeResult.length === 0) ||
30
30
  (IsActiveEntity === false && draftResult.length === 0)
31
31
  ) {
32
- req.reject(404)
32
+ req.reject(req._etagValidationClause ? 412 : 404)
33
33
  }
34
34
  }
35
35
 
@@ -46,10 +46,15 @@ const deleteDraft = async (req, srv, includingActive = false) => {
46
46
  // Intentional?
47
47
  const deleteActive = keys.IsActiveEntity !== false
48
48
 
49
- const [activeResult, draftResult] = await Promise.all([
50
- dbtx.run(_getSelectCQN(req, keys)),
51
- dbtx.run(_getDraftSelectCQN(req, keys))
52
- ])
49
+ const selectActive = _getSelectCQN(req, keys)
50
+ const selectDraft = _getDraftSelectCQN(req, keys)
51
+
52
+ if (req._etagValidationClause) {
53
+ if (keys.IsActiveEntity) selectActive.where(req._etagValidationClause)
54
+ else selectDraft.where(req._etagValidationClause)
55
+ }
56
+
57
+ const [activeResult, draftResult] = await Promise.all([dbtx.run(selectActive), dbtx.run(selectDraft)])
53
58
 
54
59
  _validate(activeResult, draftResult, req, deleteActive)
55
60
 
@@ -1,9 +1,21 @@
1
- const isNavigationToMany = req => {
2
- // REVISIT: get rid of getUriInfo
3
- if (!req.getUriInfo) return
1
+ const _ref2name = ref => (ref.id || ref).replace(/_drafts$/, '')
4
2
 
5
- const segments = req.getUriInfo().getPathSegments()
6
- return segments[segments.length - 1].getKind() === 'NAVIGATION.TO.MANY'
3
+ const isNavigationToMany = (req, model) => {
4
+ // only one segment -> false
5
+ if (req.subject.ref.length < 2) return
6
+
7
+ // last segment has it's own where -> false
8
+ if (req.subject.ref[req.subject.ref.length - 1].where) return
9
+
10
+ // determine the csn element of the last navigation and return it's is2many property
11
+ let cur
12
+ for (let i = 0; i < req.subject.ref.length - 1; i++) {
13
+ cur = cur
14
+ ? model.definitions[cur.elements[_ref2name(req.subject.ref[i])].target]
15
+ : model.definitions[_ref2name(req.subject.ref[i])]
16
+ }
17
+ const last = cur.elements[_ref2name(req.subject.ref[req.subject.ref.length - 1])]
18
+ return last.is2many
7
19
  }
8
20
 
9
21
  module.exports = {
@@ -0,0 +1,36 @@
1
+ const { ensureDraftsSuffix } = require('./handler')
2
+ const { removeIsActiveEntityRecursively, isActiveEntityRequested } = require('./where')
3
+
4
+ const _adaptSubSelectsDraft = select => {
5
+ if (select.SELECT.from.ref) {
6
+ const index = select.SELECT.from.ref.length - 1
7
+ select.SELECT.from.ref[index] = ensureDraftsSuffix(select.SELECT.from.ref[index])
8
+ }
9
+
10
+ if (select.SELECT.where) {
11
+ for (let i = 0; i < select.SELECT.where.length; i++) {
12
+ const element = select.SELECT.where[i]
13
+ if (element.SELECT) {
14
+ _adaptSubSelectsDraft(element)
15
+ } else if (element.xpr) {
16
+ _adaptSubSelectsDraft({ SELECT: { from: {}, where: element.xpr } })
17
+ }
18
+ }
19
+ }
20
+ }
21
+
22
+ const adaptStreamCQN = (cqn, isDraft = false) => {
23
+ let draft = isDraft
24
+ if (!draft) {
25
+ draft = !isActiveEntityRequested(cqn.SELECT.where)
26
+ const ref = cqn.SELECT.from?.ref
27
+ if (!draft && ref) draft = !isActiveEntityRequested(ref[ref.length - 1].where)
28
+ }
29
+
30
+ if (draft) _adaptSubSelectsDraft(cqn)
31
+ cqn.SELECT.where = removeIsActiveEntityRecursively(cqn.SELECT.where)
32
+ }
33
+
34
+ module.exports = {
35
+ adaptStreamCQN
36
+ }
@@ -57,17 +57,20 @@ class HanaDatabase extends DatabaseService {
57
57
  */
58
58
  this.on('BEGIN', function () {
59
59
  LOG._debug && LOG.debug(coloredTxCommands['BEGIN'])
60
+
60
61
  this.dbc.setAutoCommit(false)
62
+
61
63
  return 'dummy'
62
64
  })
63
65
 
64
- // REVISIT: register only if needed?
65
- this.before('COMMIT', this._integrity.performCheck)
66
-
67
66
  this.on(['COMMIT', 'ROLLBACK'], function (req) {
68
67
  LOG._debug && LOG.debug(coloredTxCommands[req.event])
68
+
69
+ // REVISIT: better?
70
+ this.dbc._closed = true
71
+
69
72
  return new Promise((resolve, reject) => {
70
- this.dbc[req.event.toLowerCase()](async err => {
73
+ this.dbc[req.event.toLowerCase()](err => {
71
74
  try {
72
75
  this.dbc.setAutoCommit(true)
73
76
  } catch (e) {
@@ -97,9 +100,6 @@ class HanaDatabase extends DatabaseService {
97
100
 
98
101
  this.before('READ', '*', localized) // > has to run after rewrite
99
102
  this.before('READ', '*', this._virtual)
100
-
101
- // REVISIT: get data to be deleted for integrity check
102
- this.before('DELETE', '*', this._integrity.beforeDelete)
103
103
  }
104
104
 
105
105
  _registerOnHandlers() {
@@ -124,8 +124,7 @@ class HanaDatabase extends DatabaseService {
124
124
  */
125
125
  // eslint-disable-next-line complexity
126
126
  async acquire(arg) {
127
- // REVISIT: remove fallback arg.user.tenant with cds^6 (still needed for cds-mtx)
128
- const tenant = arg && (typeof arg === 'string' ? arg : arg.tenant || (arg.user && arg.user.tenant))
127
+ const tenant = arg && (typeof arg === 'string' ? arg : arg.tenant)
129
128
  const dbc = await pool.acquire(tenant, this)
130
129
 
131
130
  if (arg && typeof arg !== 'string') {
@@ -152,6 +151,10 @@ class HanaDatabase extends DatabaseService {
152
151
  }
153
152
 
154
153
  dbc._tenant = tenant
154
+
155
+ // REVISIT: better?
156
+ dbc._closed = false
157
+
155
158
  return dbc
156
159
  }
157
160
 
@@ -1,5 +1,4 @@
1
- const cds = require('../cds')
2
-
1
+ const cds = require('../../../lib')
3
2
  const driver = require('./driver')
4
3
  const isHdb = driver?.name === 'hdb'
5
4
 
@@ -20,11 +19,17 @@ const convertInt64ToString = int64 => {
20
19
  }
21
20
 
22
21
  const convertToISO = element => {
23
- if (element) {
24
- return new Date(element + 'Z').toISOString()
22
+ if (!element) return null
23
+
24
+ if (cds.env.features.precise_timestamps) {
25
+ const dateTime = element.slice(0, 19).replace(' ', 'T')
26
+ let millis = element.slice(20)
27
+ if (millis.at(-1) === 'Z') millis = millis.slice(0, -1)
28
+ millis = millis.slice(0, 7).padEnd(7, '0')
29
+ return dateTime + '.' + millis + 'Z'
25
30
  }
26
31
 
27
- return null
32
+ return new Date(element + 'Z').toISOString()
28
33
  }
29
34
 
30
35
  const convertToISONoMillis = element => {
@@ -62,16 +67,6 @@ const HANA_TYPE_CONVERSION_MAP = new Map([
62
67
  ['cds.hana.CLOB', convertToString]
63
68
  ])
64
69
 
65
- if (cds.env.features.bigjs) {
66
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
67
- const Big = require('big.js')
68
- const convertToBig = value => new Big(value)
69
-
70
- HANA_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
71
- HANA_TYPE_CONVERSION_MAP.set('cds.Int64', convertToBig)
72
- HANA_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
73
- }
74
-
75
70
  module.exports = {
76
71
  HANA_TYPE_CONVERSION_MAP
77
72
  }
@@ -187,6 +187,8 @@ const _getHanaDriver = name => {
187
187
  return client.state() === 'connected'
188
188
  }
189
189
 
190
+ if (LOG._debug) LOG.debug(`Using "${driver.name}"`)
191
+
190
192
  return driver
191
193
  } catch (e) {
192
194
  if (name === 'hdb' && !isConfigured) {
@@ -100,6 +100,9 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
100
100
 
101
101
  function _getProcedureMetadata(procedureName, dbc) {
102
102
  return new Promise((resolve, reject) => {
103
+ // REVISIT: better?
104
+ if (dbc._closed) return reject(new Error('Transaction is already closed'))
105
+
103
106
  dbc.exec(
104
107
  `SELECT PARAMETER_NAME FROM SYS.PROCEDURE_PARAMETERS WHERE SCHEMA_NAME = CURRENT_SCHEMA AND PROCEDURE_NAME = '${procedureName}' AND PARAMETER_TYPE IN ('OUT', 'INOUT') ORDER BY POSITION`,
105
108
  (err, res) => {
@@ -111,6 +114,9 @@ function _getProcedureMetadata(procedureName, dbc) {
111
114
  }
112
115
 
113
116
  function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
117
+ // REVISIT: better?
118
+ if (dbc._closed) return reject(new Error('Transaction is already closed'))
119
+
114
120
  dbc.prepare(sql, async function (err, stmt) {
115
121
  if (err) {
116
122
  err.query = sql
@@ -144,6 +150,9 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
144
150
  }
145
151
  }
146
152
 
153
+ // REVISIT: better?
154
+ if (dbc._closed) return reject(new Error('Transaction is already closed'))
155
+
147
156
  // on @sap/hana-client, we need to use execQuery in case of calling procedures
148
157
  stmt[procedureName && dbc.name !== 'hdb' ? 'execQuery' : 'exec'](values, function (err, rows, ...args) {
149
158
  if (err) {
@@ -188,6 +197,9 @@ function _executeSimpleSQL(dbc, sql, values) {
188
197
  if (_hasValues(values) || !!_getProcedureName(sql)) {
189
198
  _executeAsPreparedStatement(dbc, sql, values, reject, resolve)
190
199
  } else {
200
+ // REVISIT: better?
201
+ if (dbc._closed) return reject(new Error('Transaction is already closed'))
202
+
191
203
  dbc.exec(sql, function (err, result) {
192
204
  if (err) {
193
205
  err.query = sql
@@ -317,16 +329,26 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
317
329
  })
318
330
  }
319
331
 
332
+ function _writeStream(model, dbc, query, user, locale, txTimestamp, sql, values) {
333
+ if (dbc.name === 'hdb') return writeStreamWithHdb(dbc, sql, values)
334
+
335
+ // @sap/hana-client does not support WHERE EXISTS when writing stream
336
+ const subselect = query.UPDATE.where?.find(ele => typeof ele === 'object' && ele.SELECT && ele._etagValidation)
337
+ if (!subselect) return writeStreamWithHanaClient(dbc, sql, values)
338
+
339
+ const { sql: s, values: v } = _cqnToSQL(model, subselect, user, locale, txTimestamp)
340
+ return executePlainSQL(dbc, s, v).then(res => {
341
+ if (res.length === 0) return 0
342
+ return writeStreamWithHanaClient(dbc, sql, values)
343
+ })
344
+ }
345
+
320
346
  function executeUpdateCQN(model, dbc, query, user, locale, txTimestamp) {
321
347
  const { sql, values = [] } = _cqnToSQL(model, query, user, locale, txTimestamp)
322
348
 
323
349
  // query can be insert from deep update
324
- if (query.UPDATE && hasStreamUpdate(query.UPDATE, model)) {
325
- if (dbc.name === 'hdb') {
326
- return writeStreamWithHdb(dbc, sql, values)
327
- }
328
- return writeStreamWithHanaClient(dbc, sql, values)
329
- }
350
+ if (query.UPDATE && hasStreamUpdate(query.UPDATE, model))
351
+ return _writeStream(model, dbc, query, user, locale, txTimestamp, sql, values)
330
352
 
331
353
  return _executeSimpleSQL(dbc, sql, values)
332
354
  }
@@ -1,111 +1,51 @@
1
- // REVISIT: Remove @sap/instance-manager compat with CDS 7
2
-
3
1
  const cds = require('../cds')
4
2
  const LOG = cds.log('pool|db')
5
3
 
6
4
  const { pool } = require('@sap/cds-foss')
7
5
  const hana = require('./driver')
8
-
9
- const _require = require('../common/utils/require')
10
6
  const getError = require('../common/error')
11
7
 
12
- function multiTenantServiceManager() {
13
- try {
14
- // Make sure cds-mtxs APIs are loaded
15
- require('@sap/cds-mtxs/lib') // eslint-disable-line cds/no-missing-dependencies
16
- } catch (e) {
17
- if (e.code === 'MODULE_NOT_FOUND') return null
18
- else throw e
19
- }
20
-
21
- const oldIm =
22
- cds.requires.multitenancy?.['old-instance-manager'] ??
23
- cds.env.requires?.['cds.xt.DeploymentService']?.['old-instance-manager']
24
- return oldIm ? null : cds.xt?.serviceManager
25
- }
26
-
27
- function multiTenantInstanceManager(config = cds.env.requires.db) {
28
- const { credentials } = config
29
-
30
- if (!credentials) throw Object.assign(new Error('No database credentials provided'))
31
- else if (typeof credentials !== 'object' || !(credentials.get_managed_instance_url || credentials.sm_url))
32
- throw Object.assign(new Error('Malformed database credentials provided'))
33
-
34
- // new instance manager
35
- return new Promise((resolve, reject) => {
36
- // REVISIT: better cache settings? current copied from old cds-hana...
37
- // note: may need to be low for mtx tests -> configurable?
38
- const opts = {
39
- cache_max_items: 1,
40
- cache_item_expire_seconds: 1
41
- }
42
-
43
- // check binding to both managed-hana and service-manager for instance migration
44
- if (cds.env.features.hybrid_instance_manager && process.env.VCAP_SERVICES) {
45
- const vcap = JSON.parse(process.env.VCAP_SERVICES)
46
- if (vcap['managed-hana'] && vcap['service-manager']) {
47
- opts.smOpts = vcap['service-manager'][0].credentials
48
- opts.imOpts = vcap['managed-hana'][0].credentials
49
- }
50
- }
51
-
52
- // no double config -> take passed credentials (= cds.env.requires.db.credentials)
53
- if (!opts.smOpts) Object.assign(opts, credentials)
54
-
55
- // REVISIT: should be relative
56
- // const mtxPath = require.resolve('@sap/cds-mtx', { paths: [process.env.pwd(), __dirname] })
57
- // const imPath = require.resolve('@sap/instance-manager', { paths: [mtxPath] })
58
- // _require(imPath).create(opts, (err, res) => {
59
- _require('@sap/instance-manager').create(opts, (err, res) => {
60
- if (err) return reject(err)
61
- resolve(res)
62
- })
63
- })
64
- }
65
-
66
- function singleTenantInstanceManager(config = cds.env.requires.db) {
67
- const { credentials } = config
68
-
69
- if (!credentials) throw Object.assign(new Error('No database credentials provided'))
70
- else if (typeof credentials !== 'object' || !credentials.host)
71
- throw Object.assign(new Error('Malformed database credentials provided'))
72
-
73
- // mock instance manager
74
- return {
75
- get: (_, cb) => cb(null, { credentials })
76
- }
8
+ const _getMassagedCreds = function (creds) {
9
+ if (!('ca' in creds) && creds.certificate) creds.ca = creds.certificate
10
+ if ('encrypt' in creds && !('useTLS' in creds)) creds.useTLS = creds.encrypt
11
+ if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds))
12
+ creds.sslHostNameInCertificate = creds.hostname_in_certificate
13
+ if ('validate_certificate' in creds && !('sslValidateCertificate' in creds))
14
+ creds.sslValidateCertificate = creds.validate_certificate
15
+ return creds
77
16
  }
78
17
 
79
- async function credentials4(tenant, db) {
80
- if (!db._instance_manager) {
81
- const opts = db.options && db.options.credentials ? db.options : undefined
82
- db._instance_manager = cds.requires.multitenancy
83
- ? multiTenantServiceManager() ?? (await multiTenantInstanceManager(opts))
84
- : singleTenantInstanceManager(opts)
85
- }
86
-
87
- const oldIm =
88
- cds.requires.multitenancy?.['old-instance-manager'] ??
89
- cds.env.requires?.['cds.xt.DeploymentService']?.['old-instance-manager']
90
- if (cds.xt?.serviceManager && !oldIm) {
91
- return (await db._instance_manager.get(tenant, { disableCache: true })).credentials
18
+ // NOTE: disableCache: true means "force fetch credentials from service manager"
19
+ async function credentials4(tenant, { disableCache = false }) {
20
+ const { credentials } = cds.env.requires.db
21
+ if (!credentials) throw new Error('No database credentials provided')
22
+ if (cds.requires.multitenancy) {
23
+ // eslint-disable-next-line cds/no-missing-dependencies
24
+ const res = await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache })
25
+ return _getMassagedCreds(res.credentials)
26
+ } else {
27
+ if (typeof credentials !== 'object' || !credentials.host) throw new Error('Malformed database credentials provided')
28
+ return _getMassagedCreds(credentials)
92
29
  }
93
-
94
- return new Promise((resolve, reject) => {
95
- db._instance_manager.get(tenant, (err, res) => {
96
- if (err) return reject(err)
97
- if (!res) {
98
- return reject(Object.assign(new Error(`There is no instance for tenant "${tenant}"`), { statusCode: 404 }))
99
- }
100
-
101
- resolve(res.credentials)
102
- })
103
- })
104
30
  }
105
31
 
106
32
  function factory4(creds, tenant) {
107
33
  return {
108
- create: () => hana.__connect(creds, tenant),
34
+ create: () => {
35
+ return hana.__connect(creds, tenant).catch(e => {
36
+ if (!cds.requires.multitenancy || !e?.message.match(/authentication failed/i)) throw e
37
+
38
+ LOG._warn &&
39
+ LOG.warn(
40
+ `Possibly stale credentials for tenant ${tenant}, re-trying with fresh credentials from BTP Service Manager`
41
+ )
42
+ return credentials4(tenant, { disableCache: true }).then(_creds => {
43
+ // update creds in closure
44
+ creds = _creds
45
+ return hana.__connect(creds, tenant)
46
+ })
47
+ })
48
+ },
109
49
  destroy: client => hana.__disconnect(client),
110
50
  validate: client => hana.__isConnected(client)
111
51
  }
@@ -145,27 +85,6 @@ const _getPoolConfig = function () {
145
85
  return mergedConfig
146
86
  }
147
87
 
148
- // REVISIT: copied from old cds-hana
149
- const _getMassagedCreds = function (creds) {
150
- if (!('ca' in creds) && creds.certificate) {
151
- creds.ca = creds.certificate
152
- }
153
-
154
- if ('encrypt' in creds && !('useTLS' in creds)) {
155
- creds.useTLS = creds.encrypt
156
- }
157
-
158
- if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds)) {
159
- creds.sslHostNameInCertificate = creds.hostname_in_certificate
160
- }
161
-
162
- if ('validate_certificate' in creds && !('sslValidateCertificate' in creds)) {
163
- creds.sslValidateCertificate = creds.validate_certificate
164
- }
165
-
166
- return creds
167
- }
168
-
169
88
  const pools = new Map()
170
89
 
171
90
  async function pool4(tenant, db) {
@@ -177,7 +96,7 @@ async function pool4(tenant, db) {
177
96
  .then(creds => {
178
97
  const config = _getPoolConfig()
179
98
  LOG._info && LOG.info('effective pool configuration:', config)
180
- const p = pool.createPool(factory4(_getMassagedCreds(creds), tenant), config)
99
+ const p = pool.createPool(factory4(creds, tenant), config)
181
100
 
182
101
  const INVALID_CREDENTIALS_WARNING = `Could not establish connection for tenant "${tenant}". Existing pool will be drained.`
183
102
  const INVALID_CREDENTIALS_ERROR = new Error(
@@ -250,12 +169,7 @@ async function resilientAcquire(pool, attempts = 1) {
250
169
  const message =
251
170
  'Acquiring client from pool timed out. Please review your system setup, transaction handling, and pool configuration. ' +
252
171
  `Pool State: borrowed: ${borrowed}, pending: ${pending}, size: ${size}, available: ${available}, max: ${max}`
253
- err = getError(
254
- Object.assign(err, {
255
- statusCode: 503,
256
- message
257
- })
258
- )
172
+ err = getError(Object.assign(err, { statusCode: 503, message }))
259
173
  err._attempts = attempt
260
174
  LOG._debug && LOG.debug(err)
261
175
  throw err
@@ -28,43 +28,41 @@ const search2cqn4sql = (query, entity, options) => {
28
28
  if (!cqnSearchPhrase) return query
29
29
  const localizedAssociation = entity.associations?.localized
30
30
 
31
- if (localizedAssociation) {
32
- let { columns: columns2Search = computeColumnsToBeSearched(query, entity), locale } = options
33
- const viewAlias = query.SELECT.from.as ? query.SELECT.from.as : 'LocalizedView'
34
-
35
- if (!query.SELECT.from.as) {
36
- _addAliasToQuery(query, viewAlias)
37
- }
38
-
39
- const subQuery = cds.ql.SELECT.from(entity.name).columns(1)
40
- subQuery.SELECT.from.as = targetAlias
41
- const onCondition = _generateKeysWhereCondition(entity, targetAlias, textsAlias)
42
- onCondition.push('and', { ref: [textsAlias, 'locale'] }, '=', { val: locale || "SESSION_CONTEXT('LOCALE')" })
43
-
44
- // left outer join the target table with the _texts table (the _texts table contains the translated texts)
45
- subQuery.leftJoin(localizedAssociation.target, textsAlias).on(onCondition)
46
-
47
- // add condition for equal keys of target table and localized view
48
- subQuery.where(_generateKeysWhereCondition(entity, targetAlias, viewAlias))
49
- const containsColumns = _generateContainsColumns(columns2Search, entity)
50
-
51
- let expression
52
-
53
- if (isContainsPredicateSupported(query, entity, columns2Search)) {
54
- // generate CQN expression with `CONTAINS` predicate for the columns from the target and text table
55
- expression = search2Contains(cqnSearchPhrase, containsColumns)
56
- Object.defineProperty(expression, 'searchUsingContains', { value: true, enumerable: true })
57
- } else {
58
- expression = searchToLike(cqnSearchPhrase, containsColumns)
59
- }
60
-
61
- subQuery.where(expression)
62
-
63
- // suppress the localize handler from redirecting the subQuery's target to the localized view
64
- Object.defineProperty(subQuery, '_suppressLocalization', { value: true })
65
- query.where('exists', subQuery)
66
- return query
31
+ let { columns: columns2Search = computeColumnsToBeSearched(query, entity), locale } = options
32
+ const viewAlias = query.SELECT.from.as ? query.SELECT.from.as : 'LocalizedView'
33
+
34
+ if (!query.SELECT.from.as) {
35
+ _addAliasToQuery(query, viewAlias)
67
36
  }
37
+
38
+ const subQuery = cds.ql.SELECT.from(entity.name).columns(1)
39
+ subQuery.SELECT.from.as = targetAlias
40
+ const onCondition = _generateKeysWhereCondition(entity, targetAlias, textsAlias)
41
+ onCondition.push('and', { ref: [textsAlias, 'locale'] }, '=', { val: locale || "SESSION_CONTEXT('LOCALE')" })
42
+
43
+ // left outer join the target table with the _texts table (the _texts table contains the translated texts)
44
+ subQuery.leftJoin(localizedAssociation.target, textsAlias).on(onCondition)
45
+
46
+ // add condition for equal keys of target table and localized view
47
+ subQuery.where(_generateKeysWhereCondition(entity, targetAlias, viewAlias))
48
+ const containsColumns = _generateContainsColumns(columns2Search, entity)
49
+
50
+ let expression
51
+
52
+ if (isContainsPredicateSupported(query, entity, columns2Search)) {
53
+ // generate CQN expression with `CONTAINS` predicate for the columns from the target and text table
54
+ expression = search2Contains(cqnSearchPhrase, containsColumns)
55
+ Object.defineProperty(expression, 'searchUsingContains', { value: true, enumerable: true })
56
+ } else {
57
+ expression = searchToLike(cqnSearchPhrase, containsColumns)
58
+ }
59
+
60
+ subQuery.where(expression)
61
+
62
+ // suppress the localize handler from redirecting the subQuery's target to the localized view
63
+ Object.defineProperty(subQuery, '_suppressLocalization', { value: true })
64
+ query.where('exists', subQuery)
65
+ return query
68
66
  }
69
67
 
70
68
  const _generateKeysWhereCondition = (entity, alias1, alias2) => {
@@ -3,15 +3,11 @@ const _transform = o => ({ subdomain: o.subscribedSubdomain, tenant: o.subscribe
3
3
 
4
4
  // REVISIT: Looks ugly -> can we simplify that?
5
5
  const getTenantInfo = async tenant => {
6
- const provisioningServiceName = cds.mtx ? 'ProvisioningService' : 'cds.xt.SaasProvisioningService'
7
- const primaryKey = cds.mtx ? 'ID' : 'subscribedTenantId'
8
- const path = cds.mtx ? 'tenant' : '/tenant' // HACK, otherwise the API doesn't work
9
-
10
- const provisioning = await cds.connect.to(provisioningServiceName)
6
+ const provisioning = await cds.connect.to('cds.xt.SaasProvisioningService')
11
7
  const tx = provisioning.tx({ user: new cds.User.Privileged() })
12
8
  try {
13
9
  const result = tenant
14
- ? _transform(await tx.get(path, { [primaryKey]: tenant }))
10
+ ? _transform(await tx.get('/tenant', { ['subscribedTenantId']: tenant }))
15
11
  : (await tx.read('tenant')).map(o => _transform(o))
16
12
  await tx.commit()
17
13
  return result
@@ -33,7 +33,8 @@ class EndpointRegistry {
33
33
  // unsuccessful auth doesn't automatically reject!
34
34
  paths.forEach(path => {
35
35
  cds.app.use(path, (req, res, next) => {
36
- if (!req.user) res.status(401).json(UNAUTHORIZED)
36
+ // REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
37
+ if (!req.user) res.status(401).json({ error: UNAUTHORIZED })
37
38
  next()
38
39
  })
39
40
  })
@@ -116,6 +117,7 @@ class EndpointRegistry {
116
117
  if (hasError) return res.status(500).send(results)
117
118
  return res.status(201).send(results)
118
119
  } catch (mtxError) {
120
+ // REVISIT: Still needed with cds-mtxs?
119
121
  // If an unknown tenant id is provided, cds-mtx will crash ("Cannot read property 'hanaClient' of undefined")
120
122
  return res.sendStatus(500)
121
123
  }