@sap/cds 5.7.5 → 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 (141) hide show
  1. package/CHANGELOG.md +72 -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/ql/SELECT.js +2 -2
  21. package/lib/req/cds-context.js +1 -1
  22. package/lib/req/context.js +1 -1
  23. package/lib/serve/Transaction.js +9 -5
  24. package/lib/serve/index.js +13 -21
  25. package/lib/utils/tests.js +90 -66
  26. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  27. package/libx/_runtime/auth/index.js +7 -6
  28. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  29. package/libx/_runtime/auth/utils.js +24 -0
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  54. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  55. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  56. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  60. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  61. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  62. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  63. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  64. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  65. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  66. package/libx/_runtime/common/aspects/Association.js +16 -0
  67. package/libx/_runtime/common/composition/data.js +28 -37
  68. package/libx/_runtime/common/composition/delete.js +107 -58
  69. package/libx/_runtime/common/composition/index.js +2 -1
  70. package/libx/_runtime/common/composition/insert.js +13 -13
  71. package/libx/_runtime/common/composition/update.js +39 -34
  72. package/libx/_runtime/common/error/frontend.js +17 -2
  73. package/libx/_runtime/common/generic/auth.js +20 -85
  74. package/libx/_runtime/common/generic/crud.js +22 -1
  75. package/libx/_runtime/common/i18n/messages.properties +2 -1
  76. package/libx/_runtime/common/utils/cqn.js +2 -6
  77. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  78. package/libx/_runtime/common/utils/csn.js +14 -3
  79. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  80. package/libx/_runtime/common/utils/keys.js +2 -1
  81. package/libx/_runtime/common/utils/path.js +1 -1
  82. package/libx/_runtime/common/utils/resolveView.js +12 -4
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  84. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  85. package/libx/_runtime/common/utils/vcap.js +27 -10
  86. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  87. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +8 -6
  89. package/libx/_runtime/db/expand/index.js +3 -0
  90. package/libx/_runtime/db/generic/create.js +0 -10
  91. package/libx/_runtime/db/generic/index.js +3 -0
  92. package/libx/_runtime/db/generic/read.js +2 -24
  93. package/libx/_runtime/db/generic/rewrite.js +1 -3
  94. package/libx/_runtime/db/generic/update.js +1 -1
  95. package/libx/_runtime/db/query/delete.js +10 -4
  96. package/libx/_runtime/db/query/insert.js +3 -3
  97. package/libx/_runtime/db/query/read.js +4 -1
  98. package/libx/_runtime/db/query/update.js +5 -5
  99. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  100. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  101. package/libx/_runtime/db/sql-builder/index.js +3 -0
  102. package/libx/_runtime/db/utils/columns.js +5 -2
  103. package/libx/_runtime/db/utils/deep.js +6 -8
  104. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  105. package/libx/_runtime/fiori/generic/before.js +73 -49
  106. package/libx/_runtime/fiori/generic/edit.js +14 -18
  107. package/libx/_runtime/fiori/generic/patch.js +8 -11
  108. package/libx/_runtime/fiori/generic/read.js +19 -16
  109. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  110. package/libx/_runtime/hana/Service.js +1 -1
  111. package/libx/_runtime/hana/conversion.js +10 -0
  112. package/libx/_runtime/hana/execute.js +33 -16
  113. package/libx/_runtime/hana/search.js +3 -3
  114. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  115. package/libx/_runtime/hana/searchToContains.js +1 -1
  116. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  117. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  118. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  119. package/libx/_runtime/messaging/file-based.js +3 -1
  120. package/libx/_runtime/messaging/service.js +4 -1
  121. package/libx/_runtime/remote/utils/client.js +33 -20
  122. package/libx/_runtime/remote/utils/data.js +52 -11
  123. package/libx/_runtime/sqlite/Service.js +1 -1
  124. package/libx/_runtime/sqlite/conversion.js +10 -0
  125. package/libx/_runtime/types/api.js +2 -2
  126. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  127. package/libx/odata/afterburner.js +29 -6
  128. package/libx/odata/cqn2odata.js +9 -0
  129. package/libx/odata/grammar.pegjs +49 -21
  130. package/libx/odata/index.js +2 -2
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +2 -2
  133. package/libx/rest/RestAdapter.js +29 -1
  134. package/libx/rest/middleware/auth.js +1 -3
  135. package/libx/rest/middleware/parse.js +1 -0
  136. package/package.json +1 -1
  137. package/server.js +1 -1
  138. package/bin/deploy/to-hana/logger.js +0 -27
  139. package/bin/deploy/to-hana/runCommand.js +0 -113
  140. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  141. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -17,7 +17,10 @@ const { DRAFT_COLUMNS_UNION } = require('../../../common/constants/draft')
17
17
  * @param [options.filterVirtual=false]
18
18
  * @returns {Array<object>} - array of columns
19
19
  */
20
- const getColumns = (entity, { onlyNames = false, removeIgnore = false, filterDraft = true, filterVirtual = false }) => {
20
+ const getColumns = (
21
+ entity,
22
+ { onlyNames = false, removeIgnore = false, filterDraft = true, filterVirtual = false, keysOnly = false }
23
+ ) => {
21
24
  const skipDraft = filterDraft && entity._isDraftEnabled
22
25
  const columns = []
23
26
  const elements = entity.elements
@@ -28,6 +31,7 @@ const getColumns = (entity, { onlyNames = false, removeIgnore = false, filterDra
28
31
  if (filterVirtual && element.virtual) continue
29
32
  if (removeIgnore && element['@cds.api.ignore']) continue
30
33
  if (skipDraft && DRAFT_COLUMNS_UNION.includes(each)) continue
34
+ if (keysOnly && !element.key) continue
31
35
  columns.push(onlyNames ? each : element)
32
36
  }
33
37
 
@@ -285,30 +285,29 @@ const _mergeArrays = (entity, oldValue, newValue) => {
285
285
  return merged
286
286
  }
287
287
 
288
- const mergeJsonDeep = (entity, oldValue, newValue) => {
288
+ const mergeJsonDeep = (entity, oldValue, value) => {
289
+ // REVISIT readAfterWrite -> commit -> postProcessing
290
+ // Detach result from req.data
291
+ const newValue = value ? Object.assign({}, value) : oldValue // if newValue === undefined
289
292
  if (_isObject(oldValue) && _isObject(newValue)) {
290
- Object.keys(newValue).forEach(key => {
291
- if (_isObject(newValue[key])) {
292
- if (!(key in oldValue)) Object.assign(oldValue, { [key]: newValue[key] })
293
- else {
294
- const target = entity && entity.elements[key] && entity.elements[key]._target
295
- oldValue[key] = mergeJsonDeep(target, oldValue[key], newValue[key])
296
- }
297
- } else if (Array.isArray(newValue[key])) {
298
- if (!(key in oldValue)) Object.assign(oldValue, { [key]: newValue[key] })
299
- else {
300
- const target = entity && entity.elements[key] && entity.elements[key]._target
293
+ // append to newValue to keep an order of attributes as might be defined in custom handler
294
+ Object.keys(oldValue).forEach(key => {
295
+ if (!(key in newValue)) {
296
+ Object.assign(newValue, { [key]: oldValue[key] })
297
+ } else {
298
+ const target = entity && entity.elements[key] && entity.elements[key]._target
299
+ if (_isObject(newValue[key])) {
300
+ newValue[key] = mergeJsonDeep(target, oldValue[key], newValue[key])
301
+ } else if (Array.isArray(newValue[key])) {
301
302
  if (target) {
302
- oldValue[key] = _mergeArrays(target, oldValue[key], newValue[key])
303
+ newValue[key] = _mergeArrays(target, oldValue[key], newValue[key])
303
304
  }
304
305
  // Can't merge items without target
305
306
  }
306
- } else {
307
- Object.assign(oldValue, { [key]: newValue[key] })
308
307
  }
309
308
  })
310
309
  }
311
- return oldValue
310
+ return newValue
312
311
  }
313
312
 
314
313
  // Signature similar to Object.assign(oldValue, newValue)
@@ -45,14 +45,8 @@ module.exports = class {
45
45
  }
46
46
 
47
47
  async _addPartialPersistentState(req) {
48
- const deepUpdateData = await selectDeepUpdateData(
49
- this._srv.model.definitions,
50
- req.query,
51
- req,
52
- true,
53
- true,
54
- this._srv
55
- )
48
+ // REVISIT: cds.context.model?
49
+ const deepUpdateData = await selectDeepUpdateData(this._srv, this._srv.model, req, true)
56
50
  req._.partialPersistentState = deepUpdateData
57
51
  }
58
52
 
@@ -1,11 +1,24 @@
1
1
  // global.cds is used on purpose here!
2
2
  const cds = global.cds
3
3
 
4
+ // requesting logger without module on purpose!
5
+ const LOG = cds.log()
6
+
4
7
  const ODATA_CONTAINED = '@odata.contained'
5
8
 
6
9
  const { isSelfManaged, isBacklink, getAnchor, getBacklink } = require('./utils')
7
10
  const { foreignKeyPropagations } = require('../utils/foreignKeyPropagations')
8
11
 
12
+ const _logDeprecationForODataContained = () => {
13
+ if (!cds._deprecationWarningForODataContained) {
14
+ LOG._warn &&
15
+ LOG.warn(
16
+ 'Annotation "@odata.contained" is deprecated and will be removed in an upcoming release. Use compositions instead of associations.'
17
+ )
18
+ cds._deprecationWarningForODataContained = true
19
+ }
20
+ }
21
+
9
22
  module.exports = class {
10
23
  get _isAssociationStrict() {
11
24
  return (
@@ -15,6 +28,7 @@ module.exports = class {
15
28
  }
16
29
 
17
30
  get _isAssociationEffective() {
31
+ this[ODATA_CONTAINED] && _logDeprecationForODataContained()
18
32
  return (
19
33
  this.own('__isAssociationEffective') ||
20
34
  this.set(
@@ -25,6 +39,7 @@ module.exports = class {
25
39
  }
26
40
 
27
41
  get _isCompositionEffective() {
42
+ this[ODATA_CONTAINED] && _logDeprecationForODataContained()
28
43
  return (
29
44
  this.own('__isCompositionEffective') ||
30
45
  this.set(
@@ -36,6 +51,7 @@ module.exports = class {
36
51
  }
37
52
 
38
53
  get _isContained() {
54
+ this[ODATA_CONTAINED] && _logDeprecationForODataContained()
39
55
  return (
40
56
  this.own('__isContained') ||
41
57
  this.set(
@@ -120,26 +120,26 @@ const _subWhere = (result, element) => {
120
120
  if (links && links.length > 0) {
121
121
  where = []
122
122
  for (const row of result) {
123
- if (where.length > 0) {
124
- where.push('or')
125
- }
126
123
  const whereObj = links.reduce((res, currentLink) => {
127
- if (Object.prototype.hasOwnProperty.call(row, currentLink.targetKey))
124
+ if (Object.prototype.hasOwnProperty.call(row, currentLink.targetKey) && row[currentLink.targetKey] !== null)
128
125
  res[currentLink.entityKey] = row[currentLink.targetKey]
129
126
  return res
130
127
  }, {})
131
128
  const whereCQN = ctUtils.whereKey(whereObj)
132
- if (whereCQN.length) where.push('(', ...whereCQN, ')')
129
+ if (whereCQN.length) {
130
+ if (where.length > 0) where.push('or')
131
+ where.push('(', ...whereCQN, ')')
132
+ }
133
133
  }
134
134
  }
135
135
  return where
136
136
  }
137
137
 
138
- const _mergeResults = (result, selectData, root, definitions, compositionTree, entityName) => {
138
+ const _mergeResults = (result, selectData, root, model, compositionTree, entityName) => {
139
139
  if (root) {
140
140
  return [...selectData, ...result]
141
141
  } else {
142
- const parent = definitions[compositionTree.target] || definitions[entityName]
142
+ const parent = model.definitions[compositionTree.target] || model.definitions[entityName]
143
143
  const assoc = (parent && parent.elements[compositionTree.name]) || {}
144
144
  return selectData.map(selectEntry => {
145
145
  if (assoc.is2one) {
@@ -148,8 +148,9 @@ const _mergeResults = (result, selectData, root, definitions, compositionTree, e
148
148
  selectEntry[compositionTree.name] = selectEntry[compositionTree.name] || []
149
149
  }
150
150
  const newData = _findWhere(result, _parentKey(compositionTree, selectEntry))
151
- if (assoc.is2one && newData[0]) {
152
- selectEntry[compositionTree.name] = Object.assign(selectEntry[compositionTree.name], newData[0])
151
+ if (assoc.is2one) {
152
+ if (newData[0]) selectEntry[compositionTree.name] = Object.assign(selectEntry[compositionTree.name], newData[0])
153
+ else selectEntry[compositionTree.name] = null
153
154
  } else if (assoc.is2many) {
154
155
  selectEntry[compositionTree.name].push(...newData)
155
156
  }
@@ -158,14 +159,14 @@ const _mergeResults = (result, selectData, root, definitions, compositionTree, e
158
159
  }
159
160
  }
160
161
 
161
- const _columns = (entity, data, compositionTree, selectAll) => {
162
+ const _columns = (entity, data, compositionTree, selectAllColumns) => {
162
163
  const backLinkKeys = _getLinksOfCompTree(compositionTree)
163
164
  const columns = []
164
165
  for (const elementName in entity.elements) {
165
166
  const element = entity.elements[elementName]
166
167
  if (element.virtual || element.isAssociation) continue
167
168
  if (
168
- selectAll ||
169
+ selectAllColumns ||
169
170
  element.key ||
170
171
  backLinkKeys.includes(element.name) ||
171
172
  (Array.isArray(data) && data.find(entry => element.name in entry))
@@ -177,45 +178,42 @@ const _columns = (entity, data, compositionTree, selectAll) => {
177
178
  }
178
179
 
179
180
  const _select = ({
180
- definitions,
181
+ model,
181
182
  entityName,
182
183
  draft,
183
184
  alias,
184
185
  compositionTree,
185
186
  data,
186
- root,
187
- includeAllRootColumns,
188
- includeAllColumns,
187
+ selectAllColumns,
189
188
  where,
190
189
  parentKeys,
191
190
  orderBy,
192
191
  singleton
193
192
  }) => {
194
- const entity = definitions && definitions[entityName]
193
+ const entity = model.definitions[entityName]
195
194
  const from = ctUtils.addDraftSuffix(draft, entity.name)
196
195
  const selectCQN = SELECT.from(from)
197
196
  if (alias) selectCQN.SELECT.from.as = alias
198
- const selectAll = includeAllColumns || (includeAllRootColumns && root)
199
- selectCQN.SELECT.columns = _columns(entity, data, compositionTree, selectAll)
197
+ selectCQN.SELECT.columns = _columns(entity, data, compositionTree, selectAllColumns)
200
198
  if (where) selectCQN.SELECT.where = where
201
199
  else if (parentKeys) selectCQN.SELECT.where = _whereKeys(parentKeys)
202
200
  if (orderBy) selectCQN.SELECT.orderBy = orderBy
203
201
  if (singleton) selectCQN.SELECT.limit = { rows: { val: 1 } }
204
202
  // REVISIT: remove once SELECT builder does flattening!
205
- return cqn2cqn4sql(selectCQN, { definitions })
203
+ return cqn2cqn4sql(selectCQN, model)
206
204
  }
207
205
 
208
206
  const _selectDeepUpdateData = async args => {
209
- const { definitions, compositionTree, entityName, data, includeAllColumns, root, selectData, tx } = args
207
+ const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns } = args
210
208
  const selectCQN = _select(args)
211
209
  const result = await tx.run(selectCQN)
212
210
  if (!result.length) return Promise.resolve(result)
213
211
 
214
- const keys = _keys(definitions[entityName], result)
212
+ const keys = _keys(model.definitions[entityName], result)
215
213
  await Promise.all(
216
214
  compositionTree.compositionElements.map(element => {
217
215
  if (element.skipPersistence) return Promise.resolve()
218
- if (data !== undefined && !data.find(entry => element.name in entry) && !(includeAllColumns && result.length))
216
+ if (data !== undefined && !data.find(entry => element.name in entry) && !(selectAllColumns && result.length))
219
217
  return Promise.resolve()
220
218
  const subs = {
221
219
  compositionTree: element,
@@ -235,23 +233,17 @@ const _selectDeepUpdateData = async args => {
235
233
  })
236
234
  )
237
235
 
238
- return _mergeResults(result, selectData || [], root, definitions, compositionTree, entityName)
236
+ return _mergeResults(result, selectData || [], root, model, compositionTree, entityName)
239
237
  }
240
238
 
241
239
  /*
242
240
  * exports
243
241
  */
244
242
 
245
- const selectDeepUpdateData = (
246
- definitions,
247
- cqn,
248
- req,
249
- includeAllRootColumns = false,
250
- includeAllColumns = false,
251
- service
252
- ) => {
243
+ const selectDeepUpdateData = (service, model, req, selectAllColumns = false) => {
244
+ const query = req.query
253
245
  // REVISIT this should be done somewhere before, so it is not done twice for deep updates
254
- const sqlQuery = cqn2cqn4sql(cqn, { definitions })
246
+ const sqlQuery = cqn2cqn4sql(query, model)
255
247
 
256
248
  if (req && _isSameEntity(sqlQuery, req)) {
257
249
  return Promise.resolve(req._.partialPersistentState)
@@ -263,9 +255,9 @@ const selectDeepUpdateData = (
263
255
  const entityName = ensureNoDraftsSuffix(from)
264
256
  const draft = entityName !== from
265
257
  const orderBy = req && req.target && req.target.query && req.target.query.SELECT && req.target.query.SELECT.orderBy
266
- const data = Object.assign({}, sqlQuery.UPDATE.data || {}, cqn.UPDATE.with || {})
258
+ const data = Object.assign({}, sqlQuery.UPDATE.data || {}, query.UPDATE.with || {})
267
259
  const compositionTree = getCompositionTree({
268
- definitions,
260
+ definitions: model.definitions,
269
261
  rootEntityName: entityName, // REVISIT: drafts are resolved too eagerly
270
262
  checkRoot: false,
271
263
  resolveViews: !draft,
@@ -274,17 +266,16 @@ const selectDeepUpdateData = (
274
266
 
275
267
  return _selectDeepUpdateData({
276
268
  tx: cds.tx(req),
277
- definitions,
269
+ model,
278
270
  compositionTree,
279
271
  entityName,
280
272
  data: [data],
281
273
  where,
282
274
  orderBy,
283
275
  draft,
284
- includeAllRootColumns,
285
276
  singleton: req && req.target && req.target._isSingleton,
286
277
  alias,
287
- includeAllColumns: cqn._selectAll || includeAllColumns,
278
+ selectAllColumns,
288
279
  root: true,
289
280
  service
290
281
  })
@@ -5,6 +5,9 @@ const ctUtils = require('./utils')
5
5
 
6
6
  const { ensureNoDraftsSuffix } = require('../utils/draft')
7
7
  const { getEntityNameFromDeleteCQN } = require('../utils/cqn')
8
+ const { cqn2cqn4sql } = require('../utils/cqn2cqn4sql')
9
+ const { getComp2oneParents } = require('../utils/csn')
10
+ const getColumns = require('../../db/utils/columns')
8
11
 
9
12
  /*
10
13
  * own utils
@@ -13,19 +16,21 @@ const { getEntityNameFromDeleteCQN } = require('../utils/cqn')
13
16
  // Poor man's alias algorithm
14
17
  // REVISIT: Extract and adapt the alias functionality from `expandCQNToJoin.js`: _adaptWhereOrderBy
15
18
  const _recursivelyAliasRefs = (something, newAlias, oldAlias, subselect = false) => {
16
- if (Array.isArray(something)) {
17
- for (const s of something) _recursivelyAliasRefs(s, newAlias, oldAlias, subselect)
18
- } else if (typeof something === 'object') {
19
- if (Array.isArray(something.ref)) {
19
+ if (Array.isArray(something)) return something.map(s => _recursivelyAliasRefs(s, newAlias, oldAlias, subselect))
20
+ if (typeof something === 'object') {
21
+ if (something.ref) {
22
+ something = { ref: [...something.ref] }
20
23
  if (oldAlias && something.ref[0] === oldAlias) something.ref[0] = newAlias
21
24
  else if (!subselect) something.ref.unshift(newAlias)
22
25
  } else {
26
+ something = Object.assign({}, something)
23
27
  for (const key in something) {
24
- if (key === 'from') continue // Workaround: Deep delete to be rewritten
25
- _recursivelyAliasRefs(something[key], newAlias, oldAlias, subselect || key === 'SELECT')
28
+ if (key === 'from' || key === 'val') continue // Workaround: Deep delete to be rewritten
29
+ something[key] = _recursivelyAliasRefs(something[key], newAlias, oldAlias, subselect || key === 'SELECT')
26
30
  }
27
31
  }
28
32
  }
33
+ return something
29
34
  }
30
35
 
31
36
  function _getSubWhereAndEntities(element, parentWhere, draft, level = 0, compositionTree = {}) {
@@ -88,7 +93,7 @@ function _getSubWhereAndEntities(element, parentWhere, draft, level = 0, composi
88
93
  SELECT: {
89
94
  columns: [{ val: 1, as: '_exists' }],
90
95
  from: { ref: [entity2.entityName], as: entity2.alias },
91
- where: parentWhere ? ['(', ...parentWhere, ')', 'and', '(', ...subWhere, ')'] : subWhere
96
+ where: parentWhere && parentWhere.length ? ['(', ...parentWhere, ')', 'and', '(', ...subWhere, ')'] : subWhere
92
97
  }
93
98
  })
94
99
 
@@ -130,18 +135,18 @@ function _getStaticWhere(allBackLinks, entity1) {
130
135
  }, [])
131
136
  }
132
137
 
133
- const _is2oneComposition = (element, definitions) => {
134
- const csnElement = element.target && definitions[element.target].elements[element.name]
138
+ const _is2oneComposition = (element, model) => {
139
+ const csnElement = element.target && model.definitions[element.target].elements[element.name]
135
140
  return csnElement && csnElement.is2one && csnElement._isCompositionEffective
136
141
  }
137
142
 
138
- const _addToCQNs = (cqns, subCQN, element, definitions, level) => {
143
+ const _addToCQNs = (cqns, subCQN, element, model, level) => {
139
144
  cqns[level] = cqns[level] || []
140
145
  // Since `>2.5.2` compiler generates constraints for compositions of one like for annotations
141
146
  // Thus only single 2one case (`$self`-managed composition) has DELETE CASCADE
142
147
  // Here it's ignored to simplify i.e. handle all "2ones" in a same manner
143
148
  // REVISIT: why is _db_foreign_key_constraints necessary?
144
- if (!cds.env.features._db_foreign_key_constraints || _is2oneComposition(element, definitions)) {
149
+ if (!cds.env.features._db_foreign_key_constraints || _is2oneComposition(element, model)) {
145
150
  cqns[level].push(subCQN)
146
151
  }
147
152
  }
@@ -150,15 +155,7 @@ const _addToCQNs = (cqns, subCQN, element, definitions, level) => {
150
155
  const DEEP_DELETE_MAX_RECURSION_DEPTH =
151
156
  (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 2
152
157
 
153
- const _addSubCascadeDeleteCQN = (
154
- definitions,
155
- compositionTree,
156
- parentWhere,
157
- level,
158
- cqns,
159
- draft,
160
- elementMap = new Map()
161
- ) => {
158
+ const _addSubCascadeDeleteCQN = (model, compositionTree, parentWhere, level, cqns, draft, elementMap = new Map()) => {
162
159
  for (const element of compositionTree.compositionElements) {
163
160
  if (element.skipPersistence) continue
164
161
 
@@ -176,13 +173,13 @@ const _addSubCascadeDeleteCQN = (
176
173
  if (where.length) {
177
174
  const subCQN = { DELETE: { from: { ref: [entity1.entityName], as: entity1.alias }, where: where } }
178
175
 
179
- _addToCQNs(cqns, subCQN, element, definitions, level)
176
+ _addToCQNs(cqns, subCQN, element, model, level)
180
177
 
181
178
  // Make a copy and do not share the same map among brother compositions
182
179
  // as we're only interested in deep recursions, not wide recursions.
183
180
  const newElementMap = new Map(elementMap)
184
181
  newElementMap.set(fqn, (seen && seen + 1) || 1)
185
- _addSubCascadeDeleteCQN(definitions, element, subCQN.DELETE.where, level + 1, cqns, draft, newElementMap)
182
+ _addSubCascadeDeleteCQN(model, element, subCQN.DELETE.where, level + 1, cqns, draft, newElementMap)
186
183
  }
187
184
  }
188
185
 
@@ -193,7 +190,7 @@ const _addSubCascadeDeleteCQN = (
193
190
  * exports
194
191
  */
195
192
 
196
- const hasDeepDelete = (definitions, cqn) => {
193
+ const hasDeepDelete = (model, cqn) => {
197
194
  const from = getEntityNameFromDeleteCQN(cqn)
198
195
  if (!from) return false
199
196
 
@@ -201,66 +198,118 @@ const hasDeepDelete = (definitions, cqn) => {
201
198
  // Hence, we do not need a deep delete in that case.
202
199
  if (cqn._suppressDeepDelete) return false
203
200
 
204
- const entity = definitions && definitions[ensureNoDraftsSuffix(from)]
201
+ const entity = model.definitions[ensureNoDraftsSuffix(from)]
205
202
 
206
203
  if (entity) return !!Object.keys(entity.elements || {}).find(k => entity.elements[k]._isCompositionEffective)
207
204
 
208
205
  return false
209
206
  }
210
207
 
211
- const _getSetNullParentForeignKeyCQNs = (definitions, entityName, parentWhere, draft) => {
212
- const setNullCQNs = []
213
- for (const { elements } of definitions[entityName].__oneCompositionParents.values()) {
214
- for (const element of elements.values()) {
215
- const data = element.links.reduce((d, fk) => {
216
- d[fk.entityKey] = null
217
- return d
208
+ const resolveNavigationTarget = (cqn, ref, model) => {
209
+ const lastTransition = cqn.UPDATE._transitions && cqn.UPDATE._transitions[cqn.UPDATE._transitions.length - 1]
210
+ const target = lastTransition
211
+ ? lastTransition.target
212
+ : model.definitions[(cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) || cqn.UPDATE.entity]
213
+ let elementName = ref[ref.length - 1].id || ref[ref.length - 1]
214
+ const resolved = lastTransition && lastTransition.mapping && lastTransition.mapping.get(elementName)
215
+ if (resolved) elementName = resolved.ref[0]
216
+ return { target, elementName }
217
+ }
218
+
219
+ // eslint-disable-next-line complexity
220
+ const getSetNullParentForeignKeyCQNs = async (model, req, dbQuery) => {
221
+ const cqns = []
222
+ const query = dbQuery || req.query
223
+ const origQuery = (req._tx instanceof cds.DatabaseService && req._ && req._.query) || req.query
224
+ if (!dbQuery && origQuery && origQuery.DELETE && origQuery.DELETE.from.ref && origQuery.DELETE.from.ref.length > 1) {
225
+ // delete via 2one navigation => parent is known => no need to SELECT
226
+ const ref = origQuery.DELETE.from.ref
227
+ const cqn = cqn2cqn4sql(UPDATE.entity({ ref: ref.slice(0, ref.length - 1) }), model)
228
+ const { target: parent, elementName } = resolveNavigationTarget(cqn, ref, model)
229
+ const element = parent.elements[elementName]
230
+ if (element && element._isCompositionEffective && element.is2one) {
231
+ const onCond = element && parent._relations[element.name].join('$$whatever', '$$parent')
232
+ const data = onCond.reduce((res, e) => {
233
+ if (!e.ref || e.ref[0] !== '$$parent') return res
234
+ const fk = e.ref.slice(1).join('_')
235
+ if (!parent.keys[fk]) res[fk] = null
236
+ return res
218
237
  }, {})
219
- const { entity1, where } = _getSubWhereAndEntities(element, parentWhere, draft)
220
- if (where.length) {
221
- setNullCQNs.push({
222
- UPDATE: {
223
- entity: { ref: [entity1.entityName], as: entity1.alias },
224
- data,
225
- where,
226
- _beforeDelete: true
238
+ cqn.data(data)
239
+ cqn.__4delete = true
240
+ if (Object.keys(data).length) cqns.push(cqn)
241
+ }
242
+ } else if (query.DELETE.where && query.DELETE.where.length) {
243
+ // direct or internal request => parent is uknown => need to SELECT
244
+ const entityName = getEntityNameFromDeleteCQN(query)
245
+ const target = model.definitions[entityName]
246
+ const comp2oneParents = getComp2oneParents(target, model)
247
+ for (const element of comp2oneParents) {
248
+ const parent = element.parent
249
+ const onCond = parent._relations[element.name].join('$$child', '$$parent')
250
+ const data = onCond.reduce((res, e) => {
251
+ if (!e.ref || e.ref[0] !== '$$parent') return res
252
+ const fk = e.ref.slice(1).join('_')
253
+ if (!parent.keys[fk]) res[fk] = null
254
+ return res
255
+ }, {})
256
+ const columns = getColumns(parent, { _4db: true, onlyKeys: true }).map(c => ({ ref: ['$$parent', c.name] }))
257
+ const as = query.DELETE.from.as || (query.DELETE.from.ref && query.DELETE.from.ref[0]) || query.DELETE.from
258
+ const selectCQN = SELECT.from(`${parent.name} as $$parent`)
259
+ .join(`${element.target} as $$child`)
260
+ .on(onCond)
261
+ .columns(columns)
262
+ .where(_recursivelyAliasRefs(query.DELETE.where, '$$child', as))
263
+ const results = await cds.tx(req).run(selectCQN)
264
+ if (results.length && Object.keys(data).length) {
265
+ const cqn = UPDATE.entity(parent).data(data)
266
+ for (const result of results) {
267
+ const where = []
268
+ for (const col of columns) {
269
+ if (where.length) where.push('and')
270
+ const ref = col.ref.slice(1)
271
+ where.push({ ref }, '=', { val: result[ref[0]] })
227
272
  }
228
- })
273
+ cqn.UPDATE.where = where
274
+ cqn.__4delete = true
275
+ cqns.push(cqn)
276
+ }
229
277
  }
230
278
  }
231
279
  }
232
- return setNullCQNs
280
+ return cqns
233
281
  }
234
282
 
235
- const getDeepDeleteCQNs = (definitions, cqn) => {
236
- const from = getEntityNameFromDeleteCQN(cqn)
237
- if (!from) return [[cqn]]
283
+ const getDeepDeleteCQNs = async (model, req, dbQuery) => {
284
+ const query = dbQuery || req.query
285
+ const from = getEntityNameFromDeleteCQN(query)
286
+ if (!from) return [[query]]
238
287
 
239
288
  const entityName = ensureNoDraftsSuffix(from)
240
289
  // REVISIT: baaad check!
241
290
  const draft = entityName !== from
242
291
  const compositionTree = getCompositionTree({
243
- definitions,
292
+ definitions: model.definitions,
244
293
  rootEntityName: entityName,
245
294
  checkRoot: false,
246
295
  resolveViews: !draft,
247
296
  service: cds.db
248
297
  })
249
- const parentWhere = cqn.DELETE.where && JSON.parse(JSON.stringify(cqn.DELETE.where))
250
- if (parentWhere) {
251
- const parentAlias = cqn.DELETE.from.as || (cqn.DELETE.from.ref && cqn.DELETE.from.ref[0]) || cqn.DELETE.from // or however we get the table name...
252
- _recursivelyAliasRefs(parentWhere, 'ALIAS0', parentAlias)
253
- }
254
- const setNullUpdates = []
255
- // REVISIT: why is _db_foreign_key_constraints necessary?
256
- if (cds.env.features._db_foreign_key_constraints && definitions[entityName].own('__oneCompositionParents')) {
257
- setNullUpdates.push(..._getSetNullParentForeignKeyCQNs(definitions, entityName, parentWhere, draft))
258
- }
259
- const subCascadeDeletes = _addSubCascadeDeleteCQN(definitions, compositionTree, parentWhere, 0, [], draft)
260
- return [[cqn], ...subCascadeDeletes, ...setNullUpdates].reverse()
298
+ const parentWhere = _recursivelyAliasRefs(
299
+ query.DELETE.where,
300
+ 'ALIAS0',
301
+ query.DELETE.from.as || (query.DELETE.from.ref && query.DELETE.from.ref[0]) || query.DELETE.from
302
+ )
303
+ const setNullUpdates =
304
+ (model.definitions[entityName].own('__oneCompositionParents') &&
305
+ (await getSetNullParentForeignKeyCQNs(model, req, dbQuery))) ||
306
+ []
307
+ const subCascadeDeletes = _addSubCascadeDeleteCQN(model, compositionTree, parentWhere, 0, [], draft)
308
+ return [[query], ...subCascadeDeletes, setNullUpdates].reverse()
261
309
  }
262
310
 
263
311
  module.exports = {
264
312
  hasDeepDelete,
265
- getDeepDeleteCQNs
313
+ getDeepDeleteCQNs,
314
+ getSetNullParentForeignKeyCQNs
266
315
  }
@@ -1,7 +1,7 @@
1
1
  const { getCompositionTree, getCompositionRoot } = require('./tree')
2
2
  const { hasDeepInsert, getDeepInsertCQNs, cleanEmptyCompositionsOfMany } = require('./insert')
3
3
  const { hasDeepUpdate, getDeepUpdateCQNs } = require('./update')
4
- const { hasDeepDelete, getDeepDeleteCQNs } = require('./delete')
4
+ const { hasDeepDelete, getDeepDeleteCQNs, getSetNullParentForeignKeyCQNs } = require('./delete')
5
5
  const { selectDeepUpdateData } = require('./data')
6
6
 
7
7
  module.exports = {
@@ -18,6 +18,7 @@ module.exports = {
18
18
  // delete
19
19
  hasDeepDelete,
20
20
  getDeepDeleteCQNs,
21
+ getSetNullParentForeignKeyCQNs,
21
22
  // data
22
23
  selectDeepUpdateData
23
24
  }