@sap/cds 5.7.2 → 5.8.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 (148) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/app/fiori/routes.js +1 -1
  3. package/bin/deploy/to-hana/cfUtil.js +251 -138
  4. package/bin/deploy/to-hana/gitUtil.js +55 -0
  5. package/bin/deploy/to-hana/hana.js +92 -93
  6. package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
  7. package/bin/deploy/to-hana/index.js +14 -13
  8. package/bin/mtx/in-cds.js +1 -0
  9. package/bin/serve.js +1 -1
  10. package/bin/version.js +1 -0
  11. package/lib/compile/cdsc.js +0 -6
  12. package/lib/compile/minify.js +1 -1
  13. package/lib/compile/resolve.js +1 -1
  14. package/lib/compile/to/srvinfo.js +1 -1
  15. package/lib/core/classes.js +21 -1
  16. package/lib/env/index.js +3 -2
  17. package/lib/env/requires.js +4 -0
  18. package/lib/i18n/localize.js +5 -8
  19. package/lib/index.js +1 -0
  20. package/lib/log/errors.js +1 -1
  21. package/lib/ql/SELECT.js +2 -2
  22. package/lib/req/cds-context.js +1 -1
  23. package/lib/req/context.js +1 -1
  24. package/lib/serve/Transaction.js +9 -5
  25. package/lib/serve/index.js +13 -21
  26. package/lib/utils/tests.js +90 -66
  27. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  28. package/libx/_runtime/auth/index.js +7 -6
  29. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  30. package/libx/_runtime/auth/utils.js +24 -0
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -38
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -42
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +18 -8
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +7 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  43. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +21 -30
  44. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +5 -8
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  56. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +18 -5
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +80 -21
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  62. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  63. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  64. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  65. package/libx/_runtime/cds-services/services/Service.js +1 -1
  66. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  67. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  68. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  69. package/libx/_runtime/common/aspects/Association.js +16 -0
  70. package/libx/_runtime/common/composition/data.js +28 -37
  71. package/libx/_runtime/common/composition/delete.js +107 -58
  72. package/libx/_runtime/common/composition/index.js +2 -1
  73. package/libx/_runtime/common/composition/insert.js +13 -13
  74. package/libx/_runtime/common/composition/update.js +39 -34
  75. package/libx/_runtime/common/error/frontend.js +17 -2
  76. package/libx/_runtime/common/generic/auth.js +20 -85
  77. package/libx/_runtime/common/generic/crud.js +22 -1
  78. package/libx/_runtime/common/i18n/messages.properties +3 -0
  79. package/libx/_runtime/common/utils/cqn.js +2 -6
  80. package/libx/_runtime/common/utils/cqn2cqn4sql.js +97 -123
  81. package/libx/_runtime/common/utils/csn.js +14 -3
  82. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  83. package/libx/_runtime/common/utils/keys.js +2 -1
  84. package/libx/_runtime/common/utils/path.js +1 -1
  85. package/libx/_runtime/common/utils/resolveView.js +12 -4
  86. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  87. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  88. package/libx/_runtime/common/utils/structured.js +1 -1
  89. package/libx/_runtime/common/utils/vcap.js +27 -10
  90. package/libx/_runtime/db/data-conversion/post-processing.js +42 -35
  91. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +27 -29
  93. package/libx/_runtime/db/expand/index.js +3 -0
  94. package/libx/_runtime/db/generic/create.js +0 -10
  95. package/libx/_runtime/db/generic/index.js +3 -0
  96. package/libx/_runtime/db/generic/read.js +2 -24
  97. package/libx/_runtime/db/generic/rewrite.js +1 -3
  98. package/libx/_runtime/db/generic/update.js +1 -1
  99. package/libx/_runtime/db/query/delete.js +10 -4
  100. package/libx/_runtime/db/query/insert.js +3 -3
  101. package/libx/_runtime/db/query/read.js +15 -8
  102. package/libx/_runtime/db/query/update.js +5 -5
  103. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  104. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  105. package/libx/_runtime/db/sql-builder/index.js +3 -0
  106. package/libx/_runtime/db/utils/columns.js +5 -2
  107. package/libx/_runtime/db/utils/deep.js +6 -8
  108. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  109. package/libx/_runtime/fiori/generic/before.js +73 -49
  110. package/libx/_runtime/fiori/generic/edit.js +14 -18
  111. package/libx/_runtime/fiori/generic/patch.js +8 -11
  112. package/libx/_runtime/fiori/generic/read.js +22 -17
  113. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  114. package/libx/_runtime/hana/Service.js +1 -1
  115. package/libx/_runtime/hana/conversion.js +10 -0
  116. package/libx/_runtime/hana/execute.js +33 -16
  117. package/libx/_runtime/hana/localized.js +1 -1
  118. package/libx/_runtime/hana/search.js +3 -3
  119. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  120. package/libx/_runtime/hana/searchToContains.js +1 -1
  121. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  122. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  123. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  124. package/libx/_runtime/messaging/file-based.js +3 -1
  125. package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
  126. package/libx/_runtime/messaging/service.js +16 -7
  127. package/libx/_runtime/remote/utils/client.js +33 -20
  128. package/libx/_runtime/remote/utils/data.js +53 -12
  129. package/libx/_runtime/sqlite/Service.js +1 -1
  130. package/libx/_runtime/sqlite/conversion.js +10 -0
  131. package/libx/_runtime/sqlite/localized.js +1 -1
  132. package/libx/_runtime/types/api.js +2 -2
  133. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  134. package/libx/odata/afterburner.js +29 -6
  135. package/libx/odata/cqn2odata.js +9 -0
  136. package/libx/odata/grammar.pegjs +101 -45
  137. package/libx/odata/index.js +7 -1
  138. package/libx/odata/parser.js +1 -1
  139. package/libx/odata/utils.js +2 -2
  140. package/libx/rest/RestAdapter.js +29 -1
  141. package/libx/rest/middleware/auth.js +1 -3
  142. package/libx/rest/middleware/parse.js +1 -0
  143. package/package.json +1 -1
  144. package/server.js +1 -1
  145. package/bin/deploy/to-hana/logger.js +0 -27
  146. package/bin/deploy/to-hana/runCommand.js +0 -113
  147. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  148. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -7,32 +7,10 @@
7
7
  *
8
8
  * @param req - cds.Request
9
9
  */
10
- module.exports = async function (req) {
10
+ module.exports = function (req) {
11
11
  if (typeof req.query === 'string') {
12
12
  return this._execute.sql(this.dbc, req.query, req.data)
13
13
  }
14
14
 
15
- // REVISIT: should be handled in protocol adapter
16
- // execute validation query first to fail early
17
- if (req.query._validationQuery) {
18
- const validationResult = await this._read(this.model, this.dbc, req.query._validationQuery, req)
19
-
20
- if (validationResult.length === 0) {
21
- // > validation target (e.g., root of navigation) doesn't exist
22
- req.reject(404)
23
- }
24
- }
25
-
26
- const result = await this._read(this.model, this.dbc, req.query, req)
27
-
28
- if (
29
- req.query._validationQuery &&
30
- req.query._validationQuery.__navToManyWithKeys &&
31
- (!result || result.length === 0)
32
- ) {
33
- // > navigation to collection with key specified without result -> 404
34
- req.reject(404)
35
- }
36
-
37
- return result
15
+ return this._read(this.model, this.dbc, req.query, req)
38
16
  }
@@ -1,5 +1,5 @@
1
1
  const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
2
- const generateAliases = require('../utils/generateAliases')
2
+ const { generateAliases } = require('../utils/generateAliases')
3
3
  const { restoreLink } = require('../../common/utils/resolveView')
4
4
 
5
5
  const _isLinked = req => {
@@ -18,7 +18,6 @@ function handler(req) {
18
18
  }
19
19
 
20
20
  const streaming = req.query._streaming
21
- const validationQuery = req.query._validationQuery
22
21
 
23
22
  // for restore link to req.data
24
23
  const linked = _isLinked(req)
@@ -31,7 +30,6 @@ function handler(req) {
31
30
  if (linked) restoreLink(req)
32
31
 
33
32
  if (streaming) req.query._streaming = streaming
34
- if (validationQuery) req.query._validationQuery = validationQuery
35
33
 
36
34
  generateAliases(req.query)
37
35
  }
@@ -70,7 +70,7 @@ module.exports = async function (req) {
70
70
  if (onlyKeysRemain(req)) return
71
71
 
72
72
  try {
73
- const result = await this._update(this.model, this.dbc, req.query, req)
73
+ const result = await this._update(this.model, this.dbc, req)
74
74
  return result
75
75
  } catch (err) {
76
76
  // REVISIT: db specifics
@@ -1,14 +1,14 @@
1
1
  const { getFlatArray, processCQNs } = require('../utils/deep')
2
2
  const { timestampToISO } = require('../data-conversion/timestamp')
3
- const { hasDeepDelete, getDeepDeleteCQNs } = require('../../common/composition')
3
+ const { hasDeepDelete, getDeepDeleteCQNs, getSetNullParentForeignKeyCQNs } = require('../../common/composition')
4
4
 
5
- const deleteFn = executeDeleteCQN => async (model, dbc, query, req) => {
5
+ const deleteFn = (executeDeleteCQN, executeUpdateCQN) => async (model, dbc, query, req) => {
6
6
  const { user, locale, timestamp } = req
7
7
  const isoTs = timestampToISO(timestamp)
8
8
 
9
9
  let result
10
- if (hasDeepDelete(model && model.definitions, query)) {
11
- let cqns = getDeepDeleteCQNs(model && model.definitions, query)
10
+ if (hasDeepDelete(model, query)) {
11
+ let cqns = await getDeepDeleteCQNs(model, req)
12
12
 
13
13
  // the delete chunks, i.e., how many deletes can be processed in parallel
14
14
  const chunks = []
@@ -25,6 +25,12 @@ const deleteFn = executeDeleteCQN => async (model, dbc, query, req) => {
25
25
  result = results[results.length - 1]
26
26
  } else {
27
27
  result = await executeDeleteCQN(model, dbc, query, user, locale, isoTs)
28
+ if (result) {
29
+ const updateCQNs = await getSetNullParentForeignKeyCQNs(model, req)
30
+ for (const cqn of updateCQNs) {
31
+ await executeUpdateCQN(model, dbc, cqn, user, locale, isoTs)
32
+ }
33
+ }
28
34
  }
29
35
 
30
36
  return result
@@ -6,8 +6,8 @@ const insert = executeInsertCQN => async (model, dbc, query, req) => {
6
6
  const { user, locale, timestamp } = req
7
7
  const isoTs = timestampToISO(timestamp)
8
8
 
9
- if (hasDeepInsert(model && model.definitions, query)) {
10
- const cqns = getFlatArray(getDeepInsertCQNs(model && model.definitions, query))
9
+ if (hasDeepInsert(model, query)) {
10
+ const cqns = getFlatArray(getDeepInsertCQNs(model, query))
11
11
 
12
12
  // return array of individual results
13
13
  if (cqns.length === 0) return []
@@ -15,7 +15,7 @@ const insert = executeInsertCQN => async (model, dbc, query, req) => {
15
15
  return getFlatArray(results)
16
16
  }
17
17
 
18
- cleanEmptyCompositionsOfMany(model && model.definitions, query)
18
+ cleanEmptyCompositionsOfMany(model, query)
19
19
  return executeInsertCQN(model, dbc, query, user, locale, isoTs)
20
20
  }
21
21
 
@@ -1,5 +1,7 @@
1
1
  const { timestampToISO } = require('../data-conversion/timestamp')
2
2
 
3
+ const { deepCopyObject } = require('../../common/utils/copy')
4
+
3
5
  function _arrayWithCount(a, count) {
4
6
  const _map = a.map
5
7
  const map = (..._) => _arrayWithCount(_map.call(a, ..._), count)
@@ -10,22 +12,27 @@ function _arrayWithCount(a, count) {
10
12
  }
11
13
 
12
14
  function _createCountQuery(query) {
13
- const _query = JSON.parse(JSON.stringify(query)) // REVISIT: Use query.clone() instead
14
- _query.SELECT.columns = [{ func: 'count', args: [{ val: 1 }], as: '$count' }]
15
- delete _query.SELECT.groupBy
16
- delete _query.SELECT.limit
15
+ // REVISIT: Use query.clone() instead
16
+ let _query = { SELECT: deepCopyObject(query.SELECT) }
17
17
  delete _query.SELECT.orderBy // not necessary to keep that
18
+ delete _query.SELECT.limit
18
19
  // Also change columns in sub queries
19
20
  if (_query.SELECT.from.SET) {
20
21
  _query.SELECT.from.SET.args.forEach(subCountQuery => {
21
22
  subCountQuery.SELECT.columns = [{ val: 1 }]
22
23
  })
23
24
  }
25
+ if (query.SELECT.__countAggregated) {
26
+ _query = SELECT.from(_query)
27
+ }
28
+ _query.SELECT.columns = [{ func: 'count', args: [{ val: 1 }], as: '$count' }]
24
29
  if (query.SELECT._4odata) _query.SELECT._4odata = true
25
30
  return _query
26
31
  }
27
32
 
28
- const countValue = countResult => {
33
+ const countValue = countResults => {
34
+ if (!countResults.length) return 0
35
+ const countResult = countResults[0]
29
36
  if (countResult._counted_ != null) return countResult._counted_
30
37
  if (countResult.$count != null) return countResult.$count
31
38
  }
@@ -50,11 +57,11 @@ const read = (executeSelectCQN, executeStreamCQN) => (model, dbc, query, req) =>
50
57
  const countResultPromise = executeSelectCQN(model, dbc, countQuery, user, locale, isoTs)
51
58
  if (query.SELECT.limit.rows && query.SELECT.limit.rows.val === 0) {
52
59
  // We don't need to perform our result query
53
- return countResultPromise.then(countResult => _arrayWithCount([], countValue(countResult[0])))
60
+ return countResultPromise.then(countResults => _arrayWithCount([], countValue(countResults)))
54
61
  } else {
55
62
  const resultPromise = executeSelectCQN(model, dbc, query, user, locale, isoTs)
56
- return Promise.all([countResultPromise, resultPromise]).then(([countResult, result]) =>
57
- _arrayWithCount(result, countValue(countResult[0]))
63
+ return Promise.all([countResultPromise, resultPromise]).then(([countResults, result]) =>
64
+ _arrayWithCount(result, countValue(countResults))
58
65
  )
59
66
  }
60
67
  } else {
@@ -50,11 +50,11 @@ const _getFilteredCqns = (cqns, model) => {
50
50
  return cqns
51
51
  }
52
52
 
53
- const update = (executeUpdateCQN, executeSelectCQN) => async (model, dbc, query, req) => {
54
- const { user, locale, timestamp } = req
53
+ const update = executeUpdateCQN => async (model, dbc, req) => {
54
+ const { query, user, locale, timestamp } = req
55
55
  const isoTs = timestampToISO(timestamp)
56
56
 
57
- if (hasDeepUpdate(model && model.definitions, query)) {
57
+ if (hasDeepUpdate(model, query)) {
58
58
  // REVISIT: _activeData gets set in case of draftActivate for performance, but this is a layer violation
59
59
  let selectData = req._ && req._.query && req._.query._activeData
60
60
 
@@ -62,10 +62,10 @@ const update = (executeUpdateCQN, executeSelectCQN) => async (model, dbc, query,
62
62
  selectData = [selectData]
63
63
  } else {
64
64
  // REVISIT: avoid additional read
65
- selectData = await selectDeepUpdateData(model && model.definitions, query, req, false, false, cds.db)
65
+ selectData = await selectDeepUpdateData(cds.db, model, req)
66
66
  }
67
67
 
68
- let cqns = getDeepUpdateCQNs(model && model.definitions, query, selectData)
68
+ let cqns = await getDeepUpdateCQNs(model, req, selectData)
69
69
 
70
70
  // the delete chunks, i.e., how many deletes can be processed in parallel
71
71
  const chunks = []
@@ -11,6 +11,10 @@ function _fillAfterDot(val) {
11
11
  return `${beforeDot}.${afterDot.padEnd(3, '0')}`
12
12
  }
13
13
 
14
+ function _valButNoBuffer(arg) {
15
+ return arg && arg.val && typeof arg.val === 'object' && !(arg.val instanceof Buffer)
16
+ }
17
+
14
18
  /**
15
19
  * ExpressionBuilder is used to take a part of a CQN object as an input and to build an object representing an expression
16
20
  * with SQL string and values to be used with a prepared statement.
@@ -87,9 +91,9 @@ class ExpressionBuilder extends BaseBuilder {
87
91
  }
88
92
 
89
93
  _isStructured(op1, comp, op2) {
90
- if (op1.ref && comp === '=' && op2.val && typeof op2.val === 'object' && !(op2.val instanceof Buffer)) return true
94
+ if (op1.ref && comp === '=' && _valButNoBuffer(op2)) return true
91
95
  // also check reverse
92
- if (op1.val && typeof op1.val === 'object' && comp === '=' && op2.ref && !(op1.val instanceof Buffer)) return true
96
+ if (_valButNoBuffer(op1) && comp === '=' && op2.ref) return true
93
97
  }
94
98
 
95
99
  _expressionObjectsToSQL(objects) {
@@ -379,4 +383,7 @@ class ExpressionBuilder extends BaseBuilder {
379
383
  }
380
384
  }
381
385
 
386
+ /*
387
+ * this module is required by cds-pg. -> in case of incompatible changes, we should let them know.
388
+ */
382
389
  module.exports = ExpressionBuilder
@@ -214,4 +214,7 @@ class FunctionBuilder extends BaseBuilder {
214
214
  }
215
215
  }
216
216
 
217
+ /*
218
+ * this module is required by cds-pg. -> in case of incompatible changes, we should let them know.
219
+ */
217
220
  module.exports = FunctionBuilder
@@ -9,6 +9,9 @@ const ReferenceBuilder = require('./ReferenceBuilder')
9
9
  const FunctionBuilder = require('./FunctionBuilder')
10
10
  const sqlFactory = require('./sqlFactory')
11
11
 
12
+ /*
13
+ * this module is required by cds-pg. -> in case of incompatible changes, we should let them know.
14
+ */
12
15
  module.exports = {
13
16
  CreateBuilder,
14
17
  DropBuilder,
@@ -11,7 +11,7 @@ const { DRAFT_COLUMNS } = require('../../common/constants/draft')
11
11
  * @param entity - the csn entity
12
12
  * @returns {Array} - array of columns
13
13
  */
14
- const getColumns = (entity, { db, onlyKeys } = { db: true, onlyKeys: false }) => {
14
+ const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }) => {
15
15
  // REVISIT is this correct or just a problem that occurs because of new structure we do not deal with yet?
16
16
  if (!(entity && entity.elements)) return []
17
17
  const columnNames = []
@@ -21,7 +21,7 @@ const getColumns = (entity, { db, onlyKeys } = { db: true, onlyKeys: false }) =>
21
21
  const element = elements[elementName]
22
22
  if (onlyKeys && !element.key) continue
23
23
  if (element.isAssociation) continue
24
- if (db && entity._isDraftEnabled && DRAFT_COLUMNS.includes(elementName)) continue
24
+ if (_4db && entity._isDraftEnabled && DRAFT_COLUMNS.includes(elementName)) continue
25
25
  if (cds.env.effective.odata.structs && element.elements) {
26
26
  columnNames.push(...resolveStructured({ structName: elementName, structProperties: [] }, element.elements, false))
27
27
  continue
@@ -31,4 +31,7 @@ const getColumns = (entity, { db, onlyKeys } = { db: true, onlyKeys: false }) =>
31
31
  return columnNames.map(name => elements[name] || { name })
32
32
  }
33
33
 
34
+ /*
35
+ * this module is required by cds-pg. -> in case of incompatible changes, we should let them know.
36
+ */
34
37
  module.exports = getColumns
@@ -28,20 +28,14 @@ async function processCQNs(processFn, cqns, model, dbc, user, locale, ts, chunks
28
28
  const results = new Array(cqns.length)
29
29
 
30
30
  const deletes = []
31
- const updatesBeforeDelete = []
31
+ const updatesForDeletes = []
32
32
  const others = []
33
33
  for (let i = 0; i < cqns.length; i++) {
34
34
  if (cqns[i].DELETE) deletes.push(i)
35
- else if (cqns[i].UPDATE && cqns[i].UPDATE._beforeDelete) updatesBeforeDelete.push(i)
35
+ else if (cqns[i].__4delete) updatesForDeletes.push(i)
36
36
  else others.push(i)
37
37
  }
38
38
 
39
- // UPDATEs to SET null parent's foreign keys of one compositions
40
- // which are otherwise violate foreign key constraints
41
- if (updatesBeforeDelete.length) {
42
- await _processChunk(processFn, model, dbc, cqns, user, locale, ts, updatesBeforeDelete, results)
43
- }
44
-
45
39
  if (deletes.length > 0) {
46
40
  if (chunks) {
47
41
  let offset = 0
@@ -55,6 +49,10 @@ async function processCQNs(processFn, cqns, model, dbc, user, locale, ts, chunks
55
49
  }
56
50
  }
57
51
 
52
+ if (updatesForDeletes.length > 0) {
53
+ await _processChunk(processFn, model, dbc, cqns, user, locale, ts, updatesForDeletes, results)
54
+ }
55
+
58
56
  if (others.length > 0) {
59
57
  await _processChunk(processFn, model, dbc, cqns, user, locale, ts, others, results)
60
58
  }
@@ -1,10 +1,9 @@
1
1
  const { ensureUnlocalized } = require('../../common/utils/draft')
2
-
3
2
  const ALIAS_PREFIX = 'ALIAS_'
4
-
5
- const cleanUpName = name => {
6
- return ensureUnlocalized(name).replace(/\./g, '_')
7
- }
3
+ const PARENT_ALIAS = '_parent_'
4
+ const FOREIGN_ALIAS = '_foreign_'
5
+ const PARENT_ALIAS_REGEX = new RegExp('^' + PARENT_ALIAS + '\\d*$')
6
+ const cleanUpName = name => ensureUnlocalized(name).replace(/\./g, '_')
8
7
 
9
8
  const _redirectXpr = (xpr, aliasMap) => {
10
9
  if (!xpr) return
@@ -97,4 +96,55 @@ const generateAliases = query => {
97
96
  _generateAliases(query)
98
97
  }
99
98
 
100
- module.exports = generateAliases
99
+ const _addParentAlias = (where, alias) => {
100
+ where.forEach(e => {
101
+ if (e.ref && e.ref[0].match(PARENT_ALIAS_REGEX)) {
102
+ e.ref[0] = alias
103
+ }
104
+ })
105
+ }
106
+
107
+ const _addAliasToElement = (expr, alias) => {
108
+ if (expr.ref) {
109
+ if (typeof alias === 'function') {
110
+ alias = alias(expr.ref)
111
+ }
112
+
113
+ return { ref: [alias, ...expr.ref] }
114
+ }
115
+
116
+ if (expr.list) {
117
+ return { list: expr.list.map(arg => _addAliasToElement(arg, alias)) }
118
+ }
119
+
120
+ if (expr.func) {
121
+ const args = expr.args.map(arg => _addAliasToElement(arg, alias))
122
+ return { ...expr, args }
123
+ }
124
+
125
+ if (expr.SELECT && expr.SELECT.where) {
126
+ // special case in lambda functions
127
+ _addParentAlias(expr.SELECT.where, alias)
128
+ }
129
+
130
+ if (expr.xpr) {
131
+ return { xpr: expr.xpr.map(xpr => _addAliasToElement(xpr, alias)) }
132
+ }
133
+
134
+ return expr
135
+ }
136
+
137
+ const addAliasToExpression = (expression, alias) => {
138
+ if (expression && alias) {
139
+ return expression.map(expr => _addAliasToElement(expr, alias))
140
+ }
141
+
142
+ return expression
143
+ }
144
+
145
+ module.exports = {
146
+ generateAliases,
147
+ addAliasToExpression,
148
+ PARENT_ALIAS,
149
+ FOREIGN_ALIAS
150
+ }
@@ -12,9 +12,7 @@ const { DRAFT_COLUMNS_ADMIN } = require('../../common/constants/draft')
12
12
 
13
13
  // copied from adapter/odata-v4/utils/context-object
14
14
  const _getTargetEntityName = (service, pathSegments) => {
15
- if (isCustomOperation(pathSegments, false)) {
16
- return undefined
17
- }
15
+ if (isCustomOperation(pathSegments, false)) return
18
16
 
19
17
  let navSegmentName
20
18
  let entityName = `${service.name}.${pathSegments[0].getEntitySet().getName()}`
@@ -36,12 +34,11 @@ const _getTargetEntityName = (service, pathSegments) => {
36
34
  * @returns {object}
37
35
  * @private
38
36
  */
39
- const _getParent = (service, req) => {
37
+ const _getParent = (req, service) => {
40
38
  // REVISIT: get rid of getUriInfo
41
39
  if (!req.getUriInfo) return
42
40
 
43
41
  const segments = req.getUriInfo().getPathSegments()
44
-
45
42
  if (segments.length === 1) return
46
43
 
47
44
  const parent = {
@@ -50,6 +47,7 @@ const _getParent = (service, req) => {
50
47
 
51
48
  const parentKeyPredicates = segments[segments.length - 2].getKeyPredicates()
52
49
  let keyPredicateName, keyPredicateText
50
+
53
51
  for (const keyPredicate of parentKeyPredicates) {
54
52
  keyPredicateName = keyPredicate.getEdmRef().getName()
55
53
  keyPredicateText = keyPredicate.getText()
@@ -65,36 +63,28 @@ const _getParent = (service, req) => {
65
63
  return parent
66
64
  }
67
65
 
68
- const _validateDraft = (draftResult, req) => {
69
- if (!draftResult || draftResult.length === 0) {
70
- req.reject(404)
71
- }
72
-
66
+ const _validateDraft = (req, draftResult, isBoundAction) => {
67
+ if (!draftResult || draftResult.length === 0) req.reject(404)
73
68
  const draftAdminData = draftResult[0]
74
69
 
75
- // the same user that locked the entity can always delete it
76
- if (draftAdminData.InProcessByUser === req.user.id) {
77
- return
78
- }
70
+ // the same user that locked the entity can always delete/update it
71
+ if (draftAdminData.InProcessByUser === req.user.id) return
79
72
 
80
- // proceed with the delete action only if it was initiated by a different user
81
- // than the one who locked the entity and the configured drafts cancellation
73
+ // proceed with the delete/update action only if it was initiated by a different
74
+ // user than the one who locked the entity and the configured drafts cancellation
82
75
  // timeout timer has expired
83
76
  if (draftIsLocked(draftAdminData.LastChangeDateTime)) {
84
77
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
85
78
  }
79
+
80
+ // At this point, the request user ID isn't the owner of the draft.
81
+ if (isBoundAction) req.reject(403)
86
82
  }
87
83
 
88
84
  const _addDraftDataToContext = (req, result) => {
89
- _validateDraft(result, req)
90
-
91
- if (req.rejected) {
92
- return
93
- }
94
-
95
- if (!req._draftMetadata) {
96
- req._draftMetadata = {}
97
- }
85
+ if (!result || result.length === 0) return
86
+ if (req.rejected) return
87
+ if (!req._draftMetadata) req._draftMetadata = {}
98
88
 
99
89
  DRAFT_COLUMNS_ADMIN.forEach(column => {
100
90
  if (column in result[0]) req._draftMetadata[column] = result[0][column]
@@ -103,11 +93,7 @@ const _addDraftDataToContext = (req, result) => {
103
93
  req.data.DraftAdministrativeData_DraftUUID = result[0].DraftUUID
104
94
  }
105
95
 
106
- const _prefixDraftColumns = () => {
107
- return DRAFT_COLUMNS_ADMIN.map(col => {
108
- return { ref: ['DRAFT_DraftAdministrativeData', col] }
109
- })
110
- }
96
+ const _prefixDraftColumns = () => DRAFT_COLUMNS_ADMIN.map(col => ({ ref: ['DRAFT_DraftAdministrativeData', col] }))
111
97
 
112
98
  const _getSelectDraftDataCqn = (entityName, where) => {
113
99
  return SELECT.from(ensureDraftsSuffix(entityName), _prefixDraftColumns())
@@ -116,27 +102,41 @@ const _getSelectDraftDataCqn = (entityName, where) => {
116
102
  .where(where)
117
103
  }
118
104
 
119
- const _addDraftDataFromExistingDraft = async (req, service) => {
120
- const parent = _getParent(service, req)
121
- let result
122
-
123
- if (parent && parent.IsActiveEntity === 'false') {
124
- const parentWhere = [{ ref: [parent.keyName] }, '=', { val: parent.keyValue }]
125
- result = await cds.tx(req).run(_getSelectDraftDataCqn(parent.entityName, parentWhere))
126
- _addDraftDataToContext(req, result)
127
- return result
105
+ const _getDraftDataFromExistingDraft = async (req, service, parent = _getParent(req, service)) => {
106
+ if (parent) {
107
+ if (parent.IsActiveEntity === 'false') {
108
+ const parentWhere = [{ ref: [parent.keyName] }, '=', { val: parent.keyValue }]
109
+ const query = _getSelectDraftDataCqn(parent.entityName, parentWhere)
110
+ const result = await cds.tx(req).run(query)
111
+ return result
112
+ }
113
+
114
+ return []
128
115
  }
129
116
 
130
- if (!parent) {
131
- const rootWhere = getKeysCondition(req.target, req.data)
132
- result = await cds.tx(req).run(_getSelectDraftDataCqn(ensureNoDraftsSuffix(req.target.name), rootWhere))
133
- if (result && result.length > 0) {
117
+ const rootWhere = getKeysCondition(req.target, req.data)
118
+ const query = _getSelectDraftDataCqn(ensureNoDraftsSuffix(req.target.name), rootWhere)
119
+ const result = await cds.tx(req).run(query)
120
+ return result
121
+ }
122
+
123
+ const _addDraftDataFromExistingDraft = async (req, service) => {
124
+ const parent = _getParent(req, service)
125
+ const result = await _getDraftDataFromExistingDraft(req, service, parent)
126
+
127
+ if (parent) {
128
+ if (parent.IsActiveEntity === 'false') {
129
+ _validateDraft(req, result)
134
130
  _addDraftDataToContext(req, result)
131
+ return result
135
132
  }
136
- return result
133
+
134
+ return []
137
135
  }
138
136
 
139
- return []
137
+ if (result && result.length > 0) _validateDraft(req, result)
138
+ _addDraftDataToContext(req, result)
139
+ return result
140
140
  }
141
141
 
142
142
  const _addGeneratedDraftUUID = async req => {
@@ -156,11 +156,12 @@ const _new = async function (req) {
156
156
  if (isNavigationToMany(req)) {
157
157
  const result = await _addDraftDataFromExistingDraft(req, this)
158
158
 
159
- // in order to fix strange case where active subitems are created in draft case
159
+ // in order to fix corner case where active subitems are created in draft case
160
160
  if (result.length === 0) req.reject(404)
161
- } else {
162
- _addGeneratedDraftUUID(req)
161
+ return
163
162
  }
163
+
164
+ _addGeneratedDraftUUID(req)
164
165
  }
165
166
 
166
167
  /**
@@ -173,7 +174,7 @@ const _patchUpdate = async function (req) {
173
174
 
174
175
  const result = await _addDraftDataFromExistingDraft(req, this)
175
176
 
176
- // means that draft not exists
177
+ // means that the draft does not exists
177
178
  if (result.length === 0) req.reject(404)
178
179
  }
179
180
 
@@ -186,6 +187,28 @@ const _deleteCancel = async function (req) {
186
187
  await _addDraftDataFromExistingDraft(req, this)
187
188
  }
188
189
 
190
+ const _validateDraftBoundAction = async function (req, srv) {
191
+ const result = await _getDraftDataFromExistingDraft(req, srv)
192
+ const isBoundAction = true
193
+ if (result && result.length > 0) _validateDraft(req, result, isBoundAction)
194
+ }
195
+
196
+ const _registerBoundActionHandlers = function (entityName, actions) {
197
+ if (!actions) return
198
+
199
+ const boundActions = Object.values(actions).filter(
200
+ action =>
201
+ action.kind === 'action' &&
202
+ action.name !== 'draftPrepare' &&
203
+ action.name !== 'draftEdit' &&
204
+ action.name !== 'draftActivate'
205
+ )
206
+
207
+ for (const action of boundActions) {
208
+ this.before(action.name, entityName, req => _validateDraftBoundAction(req, this))
209
+ }
210
+ }
211
+
189
212
  module.exports = cds.service.impl(function () {
190
213
  _new._initial = true
191
214
  _patchUpdate._initial = true
@@ -196,5 +219,6 @@ module.exports = cds.service.impl(function () {
196
219
  this.before('NEW', entity, _new)
197
220
  this.before(['PATCH', 'UPDATE'], entity, _patchUpdate)
198
221
  this.before(['DELETE', 'CANCEL'], entity, _deleteCancel)
222
+ _registerBoundActionHandlers.call(this, entity.name, entity.actions)
199
223
  }
200
224
  })
@@ -56,21 +56,18 @@ const _getLockWhere = (where, columnsMap) => {
56
56
  return lockWhere
57
57
  }
58
58
 
59
- const _select = async (CQNs, req, dbtx) => {
60
- let allResults
61
-
59
+ const _select = async (lockRecordCQN, draftExistsCQN, selectCQNs, req, dbtx) => {
62
60
  try {
63
- allResults = await Promise.all(CQNs.map(CQN => dbtx.run(CQN)))
64
- } catch (err) {
65
- // resource busy and NOWAIT (WAIT 0) specified (heuristic error handling method)
66
- if (err.query.includes('FOR UPDATE')) {
67
- req.reject(409, 'DRAFT_ALREADY_EXISTS')
68
- }
69
-
70
- req.reject(err)
61
+ await dbtx.run(lockRecordCQN)
62
+ } catch (e) {
63
+ const drafts = await dbtx.run(draftExistsCQN)
64
+ if (drafts.length) req.reject(409, 'DRAFT_ALREADY_EXISTS')
65
+ req.reject(409, 'ENTITY_LOCKED')
71
66
  }
72
-
73
- return allResults
67
+ const promisesResults = await Promise.allSettled([dbtx.run(draftExistsCQN), ...selectCQNs.map(cqn => dbtx.run(cqn))])
68
+ const firstRejected = promisesResults.find(r => r.status === 'rejected')
69
+ if (firstRejected) req.reject(firstRejected.reason)
70
+ return promisesResults.map(r => r.value)
74
71
  }
75
72
 
76
73
  /**
@@ -127,11 +124,9 @@ const _handler = async function (req) {
127
124
  }
128
125
  }
129
126
 
130
- const lockAndSelectCQNs = [lockRecordCQN, draftExistsCQN, ...selectCQNs]
131
-
132
127
  const dbtx = cds.tx(req)
133
128
  // REVISIT: Use service.read with expand **
134
- const [, draftExists, ...results] = await _select(lockAndSelectCQNs, req, dbtx)
129
+ const [draftExists, ...results] = await _select(lockRecordCQN, draftExistsCQN, [...selectCQNs], req, dbtx)
135
130
 
136
131
  if (!results[0].length) {
137
132
  req.reject(404)
@@ -169,12 +164,13 @@ const _handler = async function (req) {
169
164
 
170
165
  await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
171
166
  setStatusCodeAndHeader(req._.odataRes, rootWhere, req.target.name.replace(`${this.name}.`, ''), false)
172
-
173
167
  return Object.assign({}, results[0][0], { HasDraftEntity: false, HasActiveEntity: true, IsActiveEntity: false })
174
168
  }
175
169
 
176
170
  module.exports = cds.service.impl(function () {
177
- for (const entity of Object.values(this.entities).filter(e => e._isDraftEnabled)) {
171
+ const entities = Object.values(this.entities).filter(e => e._isDraftEnabled)
172
+
173
+ for (const entity of entities) {
178
174
  this.on('EDIT', entity, _handler)
179
175
  }
180
176
  })