@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
@@ -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
- const { hasDeepInsert, getDeepInsertCQNs, cleanEmptyCompositionsOfMany } = require('./insert')
2
+ const { hasDeepInsert, getDeepInsertCQNs } = 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 = {
@@ -11,13 +11,13 @@ module.exports = {
11
11
  // insert
12
12
  hasDeepInsert,
13
13
  getDeepInsertCQNs,
14
- cleanEmptyCompositionsOfMany,
15
14
  // update
16
15
  hasDeepUpdate,
17
16
  getDeepUpdateCQNs,
18
17
  // delete
19
18
  hasDeepDelete,
20
19
  getDeepDeleteCQNs,
20
+ getSetNullParentForeignKeyCQNs,
21
21
  // data
22
22
  selectDeepUpdateData
23
23
  }
@@ -10,18 +10,18 @@ const { deepCopyArray } = require('../utils/copy')
10
10
  * own utils
11
11
  */
12
12
 
13
- function _hasCompOrAssocIgnoreEmptyToMany(entity, k, data) {
13
+ function _hasCompOrAssoc(entity, k) {
14
14
  // TODO once REST also uses same logic as odata structured check if we can omit 'entity.elements[k] &&'
15
- return entity.elements[k] && (entity.elements[k].is2one || (entity.elements[k].is2many && data[k] && data[k].length))
15
+ return entity.elements[k] && (entity.elements[k].is2one || entity.elements[k].is2many)
16
16
  }
17
17
 
18
- const _addSubDeepInsertCQN = (definitions, compositionTree, data, cqns, draft) => {
18
+ const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
19
19
  compositionTree.compositionElements.forEach(element => {
20
20
  if (element.skipPersistence) {
21
21
  return
22
22
  }
23
23
  // element source must be changed in comp tree
24
- const subEntity = definitions[element.source]
24
+ const subEntity = model.definitions[element.source]
25
25
  const into = ctUtils.addDraftSuffix(draft, subEntity.name)
26
26
  const insertCQN = { INSERT: { into: into, entries: [] } }
27
27
  const subData = data.reduce((result, entry) => {
@@ -43,7 +43,7 @@ const _addSubDeepInsertCQN = (definitions, compositionTree, data, cqns, draft) =
43
43
  cqns.push(insertCQN)
44
44
  }
45
45
  if (subData.length > 0) {
46
- _addSubDeepInsertCQN(definitions, element, subData, cqns, draft)
46
+ _addSubDeepInsertCQN(model, element, subData, cqns, draft)
47
47
  }
48
48
  })
49
49
  return cqns
@@ -53,32 +53,20 @@ const _addSubDeepInsertCQN = (definitions, compositionTree, data, cqns, draft) =
53
53
  * exports
54
54
  */
55
55
 
56
- const _entityFromINSERT = (definitions, INSERT) => {
56
+ const _entityFromINSERT = (model, INSERT) => {
57
57
  if (INSERT && INSERT.into) {
58
58
  const entityName = ensureNoDraftsSuffix(INSERT.into.name || INSERT.into)
59
- return definitions && definitions[entityName]
59
+ return model.definitions[entityName]
60
60
  }
61
61
  }
62
62
 
63
- const cleanEmptyCompositionsOfMany = (definitions, cqn) => {
64
- const entity = _entityFromINSERT(definitions, cqn.INSERT)
65
- if (!entity) return
66
- for (const entry of cqn.INSERT.entries || []) {
67
- for (const elName in entry || {}) {
68
- const el = entity.elements[elName]
69
- if (!el) continue
70
- if (el.is2many && !entry[elName].length) delete entry[elName]
71
- }
72
- }
73
- }
74
-
75
- const hasDeepInsert = (definitions, cqn) => {
63
+ const hasDeepInsert = (model, cqn) => {
76
64
  if (cqn.INSERT.entries) {
77
- const entity = _entityFromINSERT(definitions, cqn.INSERT)
65
+ const entity = _entityFromINSERT(model, cqn.INSERT)
78
66
  if (entity) {
79
67
  return !!cqn.INSERT.entries.find(entry => {
80
68
  return !!Object.keys(entry || {}).find(k => {
81
- return _hasCompOrAssocIgnoreEmptyToMany(entity, k, entry)
69
+ return _hasCompOrAssoc(entity, k)
82
70
  })
83
71
  })
84
72
  }
@@ -86,14 +74,14 @@ const hasDeepInsert = (definitions, cqn) => {
86
74
  return false
87
75
  }
88
76
 
89
- const getDeepInsertCQNs = (definitions, cqn) => {
77
+ const getDeepInsertCQNs = (model, cqn) => {
90
78
  const into = cqn.INSERT.into.name || cqn.INSERT.into
91
79
  const entityName = ensureNoDraftsSuffix(into)
92
80
  const draft = entityName !== into
93
81
  const dataEntries = cqn.INSERT.entries ? deepCopyArray(cqn.INSERT.entries) : []
94
- const entity = definitions && definitions[entityName]
82
+ const entity = model.definitions[entityName]
95
83
  const compositionTree = getCompositionTree({
96
- definitions,
84
+ definitions: model.definitions,
97
85
  rootEntityName: entityName,
98
86
  checkRoot: false,
99
87
  resolveViews: !draft,
@@ -107,11 +95,10 @@ const getDeepInsertCQNs = (definitions, cqn) => {
107
95
  flattenedCqn.INSERT.entries.push(ctUtils.cleanDeepData(entity, Object.assign({}, dataEntry)))
108
96
  )
109
97
 
110
- return [flattenedCqn, ..._addSubDeepInsertCQN(definitions, compositionTree, dataEntries, [], draft)]
98
+ return [flattenedCqn, ..._addSubDeepInsertCQN(model, compositionTree, dataEntries, [], draft)]
111
99
  }
112
100
 
113
101
  module.exports = {
114
- cleanEmptyCompositionsOfMany,
115
102
  hasDeepInsert,
116
103
  getDeepInsertCQNs
117
104
  }
@@ -38,7 +38,7 @@ const _foreignKeysToLinks = (element, inverse) =>
38
38
  const _resolvedElement = (element, service) => {
39
39
  if (!element.target) return element
40
40
  // skip forbidden view check if association to view with foreign key in target
41
- const skipForbiddenViewCheck = element._isAssociationStrict && element.on && !element['@odata.contained']
41
+ const skipForbiddenViewCheck = element._isAssociationStrict && !element['@odata.contained']
42
42
  const { target, mapping } = getTransition(element._target, service, skipForbiddenViewCheck)
43
43
  const newElement = { target: target.name, _target: target }
44
44
  Object.setPrototypeOf(newElement, element)
@@ -37,6 +37,7 @@ const _dataByKey = (entity, data) => {
37
37
  function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, deleteCQN }) {
38
38
  const dataByKey = _dataByKey(entity, data)
39
39
  for (const selectEntry of selectData) {
40
+ if (!selectEntry) continue
40
41
  const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
41
42
  if (!dataEntry) {
42
43
  if (deleteCQN.DELETE.where.length > 0) {
@@ -67,7 +68,7 @@ function _fillLinkFromStructuredData(entity, entry) {
67
68
  }
68
69
  }
69
70
 
70
- const _diffData = (newData, oldData, entity, newEntry, oldEntry, definitions) => {
71
+ const _diffData = (newData, oldData, entity, newEntry, oldEntry, model) => {
71
72
  const result = {}
72
73
 
73
74
  const keysSet = new Set(Object.keys(newData).concat(Object.keys(oldData)))
@@ -96,7 +97,7 @@ const _diffData = (newData, oldData, entity, newEntry, oldEntry, definitions) =>
96
97
  nav.isComposition &&
97
98
  nav.is2one &&
98
99
  newEntry[nav.name] !== undefined &&
99
- !definitions[nav.target]._hasPersistenceSkip
100
+ !model.definitions[nav.target]._hasPersistenceSkip
100
101
  ) {
101
102
  result[key] = null
102
103
  }
@@ -106,15 +107,7 @@ const _diffData = (newData, oldData, entity, newEntry, oldEntry, definitions) =>
106
107
  return result
107
108
  }
108
109
 
109
- function _addSubDeepUpdateCQNForUpdateInsert({
110
- entity,
111
- entityName,
112
- data,
113
- selectData,
114
- updateCQNs,
115
- insertCQN,
116
- definitions
117
- }) {
110
+ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectData, updateCQNs, insertCQN, model }) {
118
111
  const selectDataByKey = _dataByKey(entity, selectData)
119
112
  const deepUpdateData = []
120
113
  for (const entry of data) {
@@ -127,7 +120,7 @@ function _addSubDeepUpdateCQNForUpdateInsert({
127
120
  deepUpdateData.push(entry)
128
121
  const newData = ctUtils.cleanDeepData(entity, entry)
129
122
  const oldData = ctUtils.cleanDeepData(entity, selectEntry)
130
- const diff = _diffData(newData, oldData, entity, entry, selectEntry, definitions)
123
+ const diff = _diffData(newData, oldData, entity, entry, selectEntry, model)
131
124
  // empty updates will be removed later
132
125
  updateCQNs.push({ UPDATE: { entity: entityName, data: diff, where: ctUtils.whereKey(key) } })
133
126
  } else {
@@ -138,7 +131,7 @@ function _addSubDeepUpdateCQNForUpdateInsert({
138
131
  return deepUpdateData
139
132
  }
140
133
 
141
- function _addSubDeepUpdateCQNCollect(definitions, cqns, updateCQNs, insertCQN, deleteCQN) {
134
+ async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQN, req) {
142
135
  if (updateCQNs.length > 0) {
143
136
  cqns[0] = cqns[0] || []
144
137
  cqns[0].push(...updateCQNs)
@@ -146,7 +139,7 @@ function _addSubDeepUpdateCQNCollect(definitions, cqns, updateCQNs, insertCQN, d
146
139
 
147
140
  if (insertCQN.INSERT.entries.length > 0) {
148
141
  cqns[0] = cqns[0] || []
149
- const deepInsertCQNs = getDeepInsertCQNs(definitions, insertCQN)
142
+ const deepInsertCQNs = getDeepInsertCQNs(model, insertCQN)
150
143
  deepInsertCQNs.forEach(insertCQN => {
151
144
  const intoCQN = cqns[0].find(cqn => {
152
145
  return cqn.INSERT && cqn.INSERT.into === insertCQN.INSERT.into
@@ -161,7 +154,7 @@ function _addSubDeepUpdateCQNCollect(definitions, cqns, updateCQNs, insertCQN, d
161
154
 
162
155
  if (deleteCQN.DELETE.where.length > 0) {
163
156
  cqns[0] = cqns[0] || []
164
- const deepDeleteCQNs = getDeepDeleteCQNs(definitions, deleteCQN)
157
+ const deepDeleteCQNs = await getDeepDeleteCQNs(model, req, deleteCQN)
165
158
  deepDeleteCQNs.forEach((deleteCQNs, index) => {
166
159
  deleteCQNs.forEach(el => {
167
160
  cqns[index] = cqns[index] || []
@@ -180,7 +173,7 @@ const _addToData = (subData, entity, element, entry) => {
180
173
  subData.push(...unwrappedSubData)
181
174
  }
182
175
 
183
- function _addSubDeepUpdateCQNRecursion({ definitions, compositionTree, entity, data, selectData, cqns, draft }) {
176
+ async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, data, selectData, cqns, draft, req }) {
184
177
  const selectDataByKey = _dataByKey(entity, selectData)
185
178
 
186
179
  for (const element of compositionTree.compositionElements) {
@@ -202,23 +195,24 @@ function _addSubDeepUpdateCQNRecursion({ definitions, compositionTree, entity, d
202
195
  }
203
196
  }
204
197
 
205
- _addSubDeepUpdateCQN({
206
- definitions,
198
+ await _addSubDeepUpdateCQN({
199
+ model,
207
200
  compositionTree: element,
208
201
  data: subData,
209
202
  selectData: selectSubData,
210
203
  cqns,
211
- draft
204
+ draft,
205
+ req
212
206
  })
213
207
  }
214
208
 
215
209
  return cqns
216
210
  }
217
211
 
218
- const _addSubDeepUpdateCQN = ({ definitions, compositionTree, data, selectData, cqns, draft }) => {
212
+ const _addSubDeepUpdateCQN = async ({ model, compositionTree, data, selectData, cqns, draft, req }) => {
219
213
  // We handle each level for deepUpdate, the moment we see that there will be an INSERT,
220
214
  // it'll be removed from our deepUpdateData (and handled deep separately).
221
- const entity = definitions && definitions[compositionTree.source]
215
+ const entity = model.definitions[compositionTree.source]
222
216
 
223
217
  if (entity._hasPersistenceSkip) return Promise.resolve()
224
218
 
@@ -234,21 +228,22 @@ const _addSubDeepUpdateCQN = ({ definitions, compositionTree, data, selectData,
234
228
  selectData,
235
229
  updateCQNs,
236
230
  insertCQN,
237
- definitions
231
+ model
238
232
  })
239
233
 
240
- _addSubDeepUpdateCQNCollect(definitions, cqns, updateCQNs, insertCQN, deleteCQN)
234
+ await _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQN, req)
241
235
 
242
236
  if (deepUpdateData.length === 0) return Promise.resolve()
243
237
 
244
238
  return _addSubDeepUpdateCQNRecursion({
245
- definitions,
239
+ model,
246
240
  compositionTree,
247
241
  entity,
248
242
  data: deepUpdateData,
249
243
  selectData,
250
244
  cqns,
251
- draft
245
+ draft,
246
+ req
252
247
  })
253
248
  }
254
249
 
@@ -256,11 +251,11 @@ const _addSubDeepUpdateCQN = ({ definitions, compositionTree, data, selectData,
256
251
  * exports
257
252
  */
258
253
 
259
- const hasDeepUpdate = (definitions, cqn) => {
254
+ const hasDeepUpdate = (model, cqn) => {
260
255
  if (cqn && cqn.UPDATE && cqn.UPDATE.entity && (cqn.UPDATE.data || cqn.UPDATE.with)) {
261
256
  const updateEntity = cqn.UPDATE.entity
262
257
  const entityName = (updateEntity.ref && updateEntity.ref[0]) || updateEntity.name || updateEntity
263
- const entity = definitions && definitions[ensureNoDraftsSuffix(entityName)]
258
+ const entity = model.definitions[ensureNoDraftsSuffix(entityName)]
264
259
 
265
260
  if (entity) {
266
261
  const keys = Object.keys(Object.assign({}, cqn.UPDATE.data || {}, cqn.UPDATE.with || {}))
@@ -271,7 +266,8 @@ const hasDeepUpdate = (definitions, cqn) => {
271
266
  return false
272
267
  }
273
268
 
274
- const getDeepUpdateCQNs = (definitions, cqn, selectData) => {
269
+ const getDeepUpdateCQNs = async (model, req, selectData) => {
270
+ const { query } = req
275
271
  if (!Array.isArray(selectData)) selectData = [selectData]
276
272
 
277
273
  if (selectData.length === 0) return []
@@ -279,22 +275,31 @@ const getDeepUpdateCQNs = (definitions, cqn, selectData) => {
279
275
  if (selectData.length > 1) throw getError('Deep update can only be performed on a single instance')
280
276
 
281
277
  const cqns = []
282
- const from = (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) || cqn.UPDATE.entity.name || cqn.UPDATE.entity
278
+ const from =
279
+ (query.UPDATE.entity.ref && query.UPDATE.entity.ref[0]) || query.UPDATE.entity.name || query.UPDATE.entity
283
280
  const entityName = ensureNoDraftsSuffix(from)
284
281
  const draft = entityName !== from
285
- const data = cqn.UPDATE.data ? deepCopyObject(cqn.UPDATE.data) : {}
286
- const withObj = cqn.UPDATE.with ? deepCopyObject(cqn.UPDATE.with) : {}
287
- const entity = definitions && definitions[entityName]
282
+ const data = query.UPDATE.data ? deepCopyObject(query.UPDATE.data) : {}
283
+ const withObj = query.UPDATE.with ? deepCopyObject(query.UPDATE.with) : {}
284
+ const entity = model.definitions[entityName]
288
285
  const entry = Object.assign({}, data, withObj, ctUtils.key(entity, selectData[0]))
289
286
  const compositionTree = getCompositionTree({
290
- definitions,
287
+ definitions: model.definitions,
291
288
  rootEntityName: entityName,
292
289
  checkRoot: false,
293
290
  resolveViews: !draft,
294
291
  service: cds.db
295
292
  })
296
293
 
297
- const subCQNs = _addSubDeepUpdateCQN({ definitions, compositionTree, data: [entry], selectData, cqns: [], draft })
294
+ const subCQNs = await _addSubDeepUpdateCQN({
295
+ model,
296
+ compositionTree,
297
+ data: [entry],
298
+ selectData,
299
+ cqns: [],
300
+ draft,
301
+ req
302
+ })
298
303
  subCQNs.forEach((subCQNs, index) => {
299
304
  cqns[index] = cqns[index] || []
300
305
  cqns[index].push(...subCQNs)
@@ -30,9 +30,8 @@ const _getFiltered = err => {
30
30
  Object.keys(err)
31
31
  .concat(['message'])
32
32
  .forEach(k => {
33
- if (k === 'innererror' && process.env.NODE_ENV === 'production') {
34
- return
35
- }
33
+ // REVISIT: do not remove innererror with cds^6
34
+ if (k === 'innererror' && process.env.NODE_ENV === 'production' && !err[SKIP_SANITIZATION]) return
36
35
  if (ALLOWED_PROPERTIES.includes(k) || k.startsWith('@')) {
37
36
  error[k] = err[k]
38
37
  } else if (k === 'numericSeverity') {
@@ -90,6 +89,20 @@ const _anonymousUser = req => {
90
89
  return Object.defineProperty(new cds.User(), '_req', { enumerable: false, value: req })
91
90
  }
92
91
 
92
+ // - for one unique value, we use it
93
+ // - if at least one 5xx exists, we use 500
94
+ // - else if at least one 4xx exists, we use 400
95
+ // - else we use 500
96
+ const _statusCodeFromDetails = details => {
97
+ const uniqueStatusCodes = new Set(
98
+ details.map(d => d.status || d.statusCode || d.code).map(c => (!isNaN(c) && Number(c)) || c)
99
+ )
100
+ if (uniqueStatusCodes.size === 1) return uniqueStatusCodes.values().next().value
101
+ if ([...uniqueStatusCodes].some(s => s >= 500)) return 500
102
+ if ([...uniqueStatusCodes].some(s => s >= 400)) return 400
103
+ return 500
104
+ }
105
+
93
106
  const normalizeError = (err, req) => {
94
107
  const user = (req && req.user) || _anonymousUser(req)
95
108
  const locale = user.locale
@@ -98,8 +111,9 @@ const normalizeError = (err, req) => {
98
111
 
99
112
  // derive status code from err status OR root code OR matching detail codes
100
113
  let statusCode = err.status || err.statusCode || (_isAllowedError(error.code) && error.code)
101
- if (!statusCode && error.details && error.details.every(ele => ele.code === error.details[0].code)) {
102
- statusCode = _isAllowedError(error.details[0].code) < 505 && error.details[0].code
114
+ if (!statusCode && error.details) {
115
+ const detailsCode = _statusCodeFromDetails(error.details)
116
+ if (_isAllowedError(detailsCode)) statusCode = detailsCode
103
117
  }
104
118
 
105
119
  // make sure it's a number