@sap/cds 5.7.5 → 5.8.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 (151) hide show
  1. package/CHANGELOG.md +97 -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/resolve.js +1 -1
  13. package/lib/compile/to/srvinfo.js +1 -1
  14. package/lib/core/classes.js +21 -1
  15. package/lib/env/index.js +3 -2
  16. package/lib/env/requires.js +4 -0
  17. package/lib/i18n/localize.js +5 -8
  18. package/lib/index.js +1 -0
  19. package/lib/log/errors.js +1 -1
  20. package/lib/log/format/kibana.js +3 -3
  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/ODataRequest.js +1 -3
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  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 +1 -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 +17 -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/ResourcePathParser.js +23 -2
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +2 -2
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
  56. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  57. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  58. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +47 -10
  64. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  65. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  66. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  67. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  68. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  69. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  70. package/libx/_runtime/common/aspects/Association.js +16 -0
  71. package/libx/_runtime/common/composition/data.js +28 -37
  72. package/libx/_runtime/common/composition/delete.js +107 -58
  73. package/libx/_runtime/common/composition/index.js +3 -3
  74. package/libx/_runtime/common/composition/insert.js +14 -27
  75. package/libx/_runtime/common/composition/tree.js +1 -1
  76. package/libx/_runtime/common/composition/update.js +39 -34
  77. package/libx/_runtime/common/error/frontend.js +19 -5
  78. package/libx/_runtime/common/generic/auth.js +20 -85
  79. package/libx/_runtime/common/generic/crud.js +22 -1
  80. package/libx/_runtime/common/i18n/messages.properties +2 -1
  81. package/libx/_runtime/common/utils/cqn.js +2 -6
  82. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  83. package/libx/_runtime/common/utils/csn.js +29 -6
  84. package/libx/_runtime/common/utils/foreignKeyPropagations.js +21 -1
  85. package/libx/_runtime/common/utils/keys.js +2 -1
  86. package/libx/_runtime/common/utils/path.js +1 -1
  87. package/libx/_runtime/common/utils/resolveView.js +12 -4
  88. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  90. package/libx/_runtime/common/utils/structured.js +10 -4
  91. package/libx/_runtime/common/utils/vcap.js +27 -10
  92. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  93. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  94. package/libx/_runtime/db/expand/expandCQNToJoin.js +67 -26
  95. package/libx/_runtime/db/expand/index.js +3 -0
  96. package/libx/_runtime/db/generic/create.js +0 -10
  97. package/libx/_runtime/db/generic/index.js +3 -0
  98. package/libx/_runtime/db/generic/read.js +2 -24
  99. package/libx/_runtime/db/generic/rewrite.js +1 -3
  100. package/libx/_runtime/db/generic/update.js +1 -1
  101. package/libx/_runtime/db/query/delete.js +10 -4
  102. package/libx/_runtime/db/query/insert.js +3 -4
  103. package/libx/_runtime/db/query/read.js +4 -1
  104. package/libx/_runtime/db/query/update.js +5 -5
  105. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  106. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  107. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
  108. package/libx/_runtime/db/sql-builder/index.js +3 -0
  109. package/libx/_runtime/db/utils/columns.js +5 -2
  110. package/libx/_runtime/db/utils/deep.js +16 -14
  111. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  112. package/libx/_runtime/fiori/generic/before.js +73 -49
  113. package/libx/_runtime/fiori/generic/edit.js +14 -18
  114. package/libx/_runtime/fiori/generic/patch.js +8 -11
  115. package/libx/_runtime/fiori/generic/read.js +20 -19
  116. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  117. package/libx/_runtime/fiori/utils/handler.js +1 -11
  118. package/libx/_runtime/hana/Service.js +1 -1
  119. package/libx/_runtime/hana/conversion.js +12 -1
  120. package/libx/_runtime/hana/dynatrace.js +11 -5
  121. package/libx/_runtime/hana/execute.js +132 -19
  122. package/libx/_runtime/hana/search.js +3 -3
  123. package/libx/_runtime/hana/search2cqn4sql.js +23 -25
  124. package/libx/_runtime/hana/searchToContains.js +1 -1
  125. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  126. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  127. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  128. package/libx/_runtime/messaging/file-based.js +3 -1
  129. package/libx/_runtime/messaging/service.js +4 -1
  130. package/libx/_runtime/remote/utils/client.js +41 -24
  131. package/libx/_runtime/remote/utils/data.js +54 -12
  132. package/libx/_runtime/sqlite/Service.js +1 -1
  133. package/libx/_runtime/sqlite/conversion.js +10 -0
  134. package/libx/_runtime/types/api.js +2 -2
  135. package/libx/gql/resolvers/crud/update.js +8 -5
  136. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  137. package/libx/odata/afterburner.js +29 -6
  138. package/libx/odata/cqn2odata.js +9 -0
  139. package/libx/odata/grammar.pegjs +49 -21
  140. package/libx/odata/index.js +2 -2
  141. package/libx/odata/parser.js +1 -1
  142. package/libx/odata/utils.js +2 -2
  143. package/libx/rest/RestAdapter.js +29 -1
  144. package/libx/rest/middleware/auth.js +1 -3
  145. package/libx/rest/middleware/parse.js +1 -0
  146. package/package.json +1 -1
  147. package/server.js +1 -1
  148. package/bin/deploy/to-hana/logger.js +0 -27
  149. package/bin/deploy/to-hana/runCommand.js +0 -113
  150. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  151. package/libx/_runtime/common/utils/auth.js +0 -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
@@ -1,4 +1,4 @@
1
- const { hasDeepInsert, getDeepInsertCQNs, cleanEmptyCompositionsOfMany } = require('../../common/composition')
1
+ const { hasDeepInsert, getDeepInsertCQNs } = require('../../common/composition')
2
2
  const { getFlatArray, processCQNs } = require('../utils/deep')
3
3
  const { timestampToISO } = require('../data-conversion/timestamp')
4
4
 
@@ -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,6 @@ const insert = executeInsertCQN => async (model, dbc, query, req) => {
15
15
  return getFlatArray(results)
16
16
  }
17
17
 
18
- cleanEmptyCompositionsOfMany(model && model.definitions, query)
19
18
  return executeInsertCQN(model, dbc, query, user, locale, isoTs)
20
19
  }
21
20
 
@@ -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,7 +12,8 @@ function _arrayWithCount(a, count) {
10
12
  }
11
13
 
12
14
  function _createCountQuery(query) {
13
- let _query = JSON.parse(JSON.stringify(query)) // REVISIT: Use query.clone() instead
15
+ // REVISIT: Use query.clone() instead
16
+ let _query = { SELECT: deepCopyObject(query.SELECT) }
14
17
  delete _query.SELECT.orderBy // not necessary to keep that
15
18
  delete _query.SELECT.limit
16
19
  // Also change columns in sub queries
@@ -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
@@ -96,7 +96,7 @@ class SelectBuilder extends BaseBuilder {
96
96
  }
97
97
 
98
98
  if (this._obj.SELECT.orderBy && this._obj.SELECT.orderBy.length) {
99
- this._orderBy()
99
+ this._orderBy(noQuoting)
100
100
  }
101
101
 
102
102
  if (this._obj.SELECT.limit || this._obj.SELECT.one) {
@@ -373,7 +373,11 @@ class SelectBuilder extends BaseBuilder {
373
373
  this._outputObj.values.push(...values)
374
374
  }
375
375
 
376
- _orderBy() {
376
+ _getOrderByElement(noQuoting, name, element) {
377
+ return (noQuoting ? name : this._quote(name)) + ' ' + (element.sort || 'asc').toUpperCase()
378
+ }
379
+
380
+ _orderBy(noQuoting) {
377
381
  const sqls = []
378
382
  this._outputObj.sql.push('ORDER BY')
379
383
  for (const element of this._obj.SELECT.orderBy) {
@@ -385,7 +389,7 @@ class SelectBuilder extends BaseBuilder {
385
389
  if (!columns.find(c => JSON.stringify(c.ref) === serialized)) {
386
390
  const toMatch = element.as || (element.ref && element.ref.length === 1 && element.ref[0])
387
391
  if (toMatch && columns.find(c => c.as === toMatch)) {
388
- sqls.push(this._quote(toMatch) + ' ' + (element.sort || 'asc').toUpperCase())
392
+ sqls.push(this._getOrderByElement(noQuoting, toMatch, element))
389
393
  continue
390
394
  }
391
395
  }
@@ -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
@@ -1,14 +1,18 @@
1
- function _flattenDeep(arr) {
2
- return arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? _flattenDeep(val) : val), [])
1
+ const _flattenDeep = (arr, res) => {
2
+ if (!Array.isArray(arr)) {
3
+ res.push(arr)
4
+ return res
5
+ }
6
+ for (const a of arr) {
7
+ _flattenDeep(a, res)
8
+ }
9
+ return res
3
10
  }
4
11
 
5
12
  /*
6
13
  * flatten with a dfs approach. this is important!!!
7
14
  */
8
- function getFlatArray(arg) {
9
- if (!Array.isArray(arg)) return [arg]
10
- return _flattenDeep(arg)
11
- }
15
+ const getFlatArray = arg => _flattenDeep(arg, [])
12
16
 
13
17
  async function _processChunk(processFn, model, dbc, cqns, user, locale, ts, indexes, results) {
14
18
  const promises = []
@@ -28,20 +32,14 @@ async function processCQNs(processFn, cqns, model, dbc, user, locale, ts, chunks
28
32
  const results = new Array(cqns.length)
29
33
 
30
34
  const deletes = []
31
- const updatesBeforeDelete = []
35
+ const updatesForDeletes = []
32
36
  const others = []
33
37
  for (let i = 0; i < cqns.length; i++) {
34
38
  if (cqns[i].DELETE) deletes.push(i)
35
- else if (cqns[i].UPDATE && cqns[i].UPDATE._beforeDelete) updatesBeforeDelete.push(i)
39
+ else if (cqns[i].__4delete) updatesForDeletes.push(i)
36
40
  else others.push(i)
37
41
  }
38
42
 
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
43
  if (deletes.length > 0) {
46
44
  if (chunks) {
47
45
  let offset = 0
@@ -55,6 +53,10 @@ async function processCQNs(processFn, cqns, model, dbc, user, locale, ts, chunks
55
53
  }
56
54
  }
57
55
 
56
+ if (updatesForDeletes.length > 0) {
57
+ await _processChunk(processFn, model, dbc, cqns, user, locale, ts, updatesForDeletes, results)
58
+ }
59
+
58
60
  if (others.length > 0) {
59
61
  await _processChunk(processFn, model, dbc, cqns, user, locale, ts, others, results)
60
62
  }
@@ -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
  })