@sap/cds 5.7.2 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/app/fiori/routes.js +1 -1
  3. package/bin/deploy/to-hana/cfUtil.js +251 -138
  4. package/bin/deploy/to-hana/gitUtil.js +55 -0
  5. package/bin/deploy/to-hana/hana.js +92 -93
  6. package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
  7. package/bin/deploy/to-hana/index.js +14 -13
  8. package/bin/mtx/in-cds.js +1 -0
  9. package/bin/serve.js +1 -1
  10. package/bin/version.js +1 -0
  11. package/lib/compile/cdsc.js +0 -6
  12. package/lib/compile/minify.js +1 -1
  13. package/lib/compile/resolve.js +1 -1
  14. package/lib/compile/to/srvinfo.js +1 -1
  15. package/lib/core/classes.js +21 -1
  16. package/lib/env/index.js +3 -2
  17. package/lib/env/requires.js +4 -0
  18. package/lib/i18n/localize.js +5 -8
  19. package/lib/index.js +1 -0
  20. package/lib/log/errors.js +1 -1
  21. package/lib/ql/SELECT.js +2 -2
  22. package/lib/req/cds-context.js +1 -1
  23. package/lib/req/context.js +1 -1
  24. package/lib/serve/Transaction.js +9 -5
  25. package/lib/serve/index.js +13 -21
  26. package/lib/utils/tests.js +90 -66
  27. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  28. package/libx/_runtime/auth/index.js +7 -6
  29. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  30. package/libx/_runtime/auth/utils.js +24 -0
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -38
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -42
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +18 -8
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +7 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  43. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +21 -30
  44. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +5 -8
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  56. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +18 -5
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +80 -21
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  62. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  63. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  64. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  65. package/libx/_runtime/cds-services/services/Service.js +1 -1
  66. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  67. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  68. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  69. package/libx/_runtime/common/aspects/Association.js +16 -0
  70. package/libx/_runtime/common/composition/data.js +28 -37
  71. package/libx/_runtime/common/composition/delete.js +107 -58
  72. package/libx/_runtime/common/composition/index.js +2 -1
  73. package/libx/_runtime/common/composition/insert.js +13 -13
  74. package/libx/_runtime/common/composition/update.js +39 -34
  75. package/libx/_runtime/common/error/frontend.js +17 -2
  76. package/libx/_runtime/common/generic/auth.js +20 -85
  77. package/libx/_runtime/common/generic/crud.js +22 -1
  78. package/libx/_runtime/common/i18n/messages.properties +3 -0
  79. package/libx/_runtime/common/utils/cqn.js +2 -6
  80. package/libx/_runtime/common/utils/cqn2cqn4sql.js +97 -123
  81. package/libx/_runtime/common/utils/csn.js +14 -3
  82. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  83. package/libx/_runtime/common/utils/keys.js +2 -1
  84. package/libx/_runtime/common/utils/path.js +1 -1
  85. package/libx/_runtime/common/utils/resolveView.js +12 -4
  86. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  87. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  88. package/libx/_runtime/common/utils/structured.js +1 -1
  89. package/libx/_runtime/common/utils/vcap.js +27 -10
  90. package/libx/_runtime/db/data-conversion/post-processing.js +42 -35
  91. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +27 -29
  93. package/libx/_runtime/db/expand/index.js +3 -0
  94. package/libx/_runtime/db/generic/create.js +0 -10
  95. package/libx/_runtime/db/generic/index.js +3 -0
  96. package/libx/_runtime/db/generic/read.js +2 -24
  97. package/libx/_runtime/db/generic/rewrite.js +1 -3
  98. package/libx/_runtime/db/generic/update.js +1 -1
  99. package/libx/_runtime/db/query/delete.js +10 -4
  100. package/libx/_runtime/db/query/insert.js +3 -3
  101. package/libx/_runtime/db/query/read.js +15 -8
  102. package/libx/_runtime/db/query/update.js +5 -5
  103. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  104. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  105. package/libx/_runtime/db/sql-builder/index.js +3 -0
  106. package/libx/_runtime/db/utils/columns.js +5 -2
  107. package/libx/_runtime/db/utils/deep.js +6 -8
  108. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  109. package/libx/_runtime/fiori/generic/before.js +73 -49
  110. package/libx/_runtime/fiori/generic/edit.js +14 -18
  111. package/libx/_runtime/fiori/generic/patch.js +8 -11
  112. package/libx/_runtime/fiori/generic/read.js +22 -17
  113. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  114. package/libx/_runtime/hana/Service.js +1 -1
  115. package/libx/_runtime/hana/conversion.js +10 -0
  116. package/libx/_runtime/hana/execute.js +33 -16
  117. package/libx/_runtime/hana/localized.js +1 -1
  118. package/libx/_runtime/hana/search.js +3 -3
  119. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  120. package/libx/_runtime/hana/searchToContains.js +1 -1
  121. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  122. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  123. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  124. package/libx/_runtime/messaging/file-based.js +3 -1
  125. package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
  126. package/libx/_runtime/messaging/service.js +16 -7
  127. package/libx/_runtime/remote/utils/client.js +33 -20
  128. package/libx/_runtime/remote/utils/data.js +53 -12
  129. package/libx/_runtime/sqlite/Service.js +1 -1
  130. package/libx/_runtime/sqlite/conversion.js +10 -0
  131. package/libx/_runtime/sqlite/localized.js +1 -1
  132. package/libx/_runtime/types/api.js +2 -2
  133. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  134. package/libx/odata/afterburner.js +29 -6
  135. package/libx/odata/cqn2odata.js +9 -0
  136. package/libx/odata/grammar.pegjs +101 -45
  137. package/libx/odata/index.js +7 -1
  138. package/libx/odata/parser.js +1 -1
  139. package/libx/odata/utils.js +2 -2
  140. package/libx/rest/RestAdapter.js +29 -1
  141. package/libx/rest/middleware/auth.js +1 -3
  142. package/libx/rest/middleware/parse.js +1 -0
  143. package/package.json +1 -1
  144. package/server.js +1 -1
  145. package/bin/deploy/to-hana/logger.js +0 -27
  146. package/bin/deploy/to-hana/runCommand.js +0 -113
  147. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  148. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -5,7 +5,7 @@
5
5
  const cds = require('../../cds')
6
6
  const { SELECT } = cds.ql
7
7
 
8
- const { getRequiresAsArray } = require('../utils/auth')
8
+ const { getRequiresAsArray } = require('../../auth/utils')
9
9
  const { cqn2cqn4sql } = require('../utils/cqn2cqn4sql')
10
10
  const { isActiveEntityRequested, removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
11
11
  const { ensureNoDraftsSuffix } = require('../../fiori/utils/handler')
@@ -226,88 +226,6 @@ const _getMergedWhere = restricts => {
226
226
  return xprs
227
227
  }
228
228
 
229
- const _findTableName = (ref, aliases) => {
230
- const maxLength = Math.max(...aliases.map(alias => alias.length))
231
- let name = ''
232
- for (let i = 0; i < ref.length; i++) {
233
- name += name.length !== 0 ? `.${ref[i]}` : ref[i]
234
-
235
- if (name >= maxLength) {
236
- break
237
- }
238
-
239
- const aliasIndex = aliases.indexOf(name)
240
- if (aliasIndex !== -1) {
241
- return { refIndex: i, aliasIndex: aliasIndex, name: name }
242
- }
243
- }
244
-
245
- return { refIndex: -1 }
246
- }
247
-
248
- const _getTableForColumn = (col, aliases, model) => {
249
- for (let i = 0; i < aliases.length; i++) {
250
- const index = aliases.length - i - 1
251
- const alias = aliases[index]
252
- if (Object.keys(model.definitions[alias].elements).includes(col)) {
253
- return { index, table: alias.replace(/\./g, '_') }
254
- }
255
- }
256
-
257
- return { index: -1 }
258
- }
259
-
260
- const _adaptTableName = (ref, index, name) => {
261
- const tableName = name.replace(/\./g, '_')
262
- ref.splice(0, index + 1, tableName)
263
- }
264
-
265
- const _ensureTableAlias = (ref, aliases, targetFrom, model, hasExpand) => {
266
- const nameObj = _findTableName(ref, aliases)
267
- if (nameObj.refIndex === -1) {
268
- const { index, table } = _getTableForColumn(ref[0], aliases, model)
269
- if (index !== -1) {
270
- nameObj.aliasIndex = index
271
- if (table === targetFrom.name && targetFrom.as) {
272
- ref.unshift(targetFrom.as)
273
- } else {
274
- ref.unshift(table)
275
- }
276
- }
277
- } else {
278
- _adaptTableName(ref, nameObj.refIndex, nameObj.name)
279
- }
280
- }
281
-
282
- const _enhanceAnnotationSubSelect = (select, model, targetName, targetFrom, hasExpand) => {
283
- if (select.where) {
284
- for (const v of select.where) {
285
- if (v.ref && select.from.ref) {
286
- _ensureTableAlias(v.ref, [targetName, select.from.ref[0]], targetFrom, model, hasExpand)
287
- }
288
- }
289
- }
290
- }
291
-
292
- // Add alias symbols to refs if needed and mark ref (for expand) and SELECT.from (for draft)
293
- const _enhanceAnnotationWhere = (query, where, model) => {
294
- const cqn2cqn4sqlOptions = { suppressSearch: true }
295
- query = cqn2cqn4sql(query, model, cqn2cqn4sqlOptions)
296
- const hasExpand = query.SELECT && query.SELECT.columns && query.SELECT.columns.some(col => col.expand)
297
- const targetFrom = query.SELECT
298
- ? { name: query.SELECT.from.ref[0].replace(/\./g, '_'), as: query.SELECT.from.as }
299
- : {}
300
- for (const w of where) {
301
- if (w.ref) {
302
- // REVISIT: can this case be removed permanently?
303
- // _ensureTableAlias(w.ref, [query._target.name], targetFrom, model, hasExpand)
304
- } else if (w.SELECT) {
305
- _enhanceAnnotationSubSelect(w.SELECT, model, query._target.name, targetFrom, hasExpand)
306
- w.SELECT.__targetFrom = targetFrom
307
- }
308
- }
309
- }
310
-
311
229
  const _getApplicables = (restricts, req) => {
312
230
  return restricts.filter(restrict => {
313
231
  const event = DRAFT2CRUD[req.event] || req.event
@@ -419,9 +337,26 @@ const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
419
337
 
420
338
  const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
421
339
  if (restrictionForTarget) {
340
+ // adjust free subselects, if necessary
341
+ if (resolvedApplicables.some(ra => ra.where.match(/\s*exists\s*\(\s*select\s*1\s*/i))) {
342
+ for (const ele of restrictionForTarget) {
343
+ if (typeof ele !== 'object' || !ele.SELECT || !ele.SELECT.where) continue
344
+ for (const w of ele.SELECT.where) {
345
+ if (w.ref && w.ref.length > 2) {
346
+ let path = w.ref[0]
347
+ if (!model.definitions[path]) continue
348
+ let i = 1
349
+ for (; i < w.ref.length; i++) {
350
+ if (model.definitions[`${path}.${w.ref[i]}`]) path += `.${w.ref[i]}`
351
+ else break
352
+ }
353
+ w.ref = [path, ...w.ref.slice(i)]
354
+ }
355
+ }
356
+ }
357
+ }
358
+ // apply restriction
422
359
  req.query.where(restrictionForTarget)
423
- // REVISIT: remove with cds^6
424
- _enhanceAnnotationWhere(req.query, restrictionForTarget, model)
425
360
  }
426
361
  }
427
362
 
@@ -4,6 +4,7 @@ const { SELECT } = cds.ql
4
4
  const getTemplate = require('../utils/template')
5
5
  const templateProcessor = require('../utils/templateProcessor')
6
6
  const replaceManagedData = require('../utils/dollar')
7
+ const { deepCopyArray } = require('../utils/copy')
7
8
 
8
9
  const onlyKeysRemain = require('../utils/onlyKeysRemain')
9
10
 
@@ -67,6 +68,7 @@ const _updateReqData = (req, that) => {
67
68
  }
68
69
 
69
70
  module.exports = cds.service.impl(function () {
71
+ // eslint-disable-next-line complexity
70
72
  this.on(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', async function (req) {
71
73
  if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
72
74
  req.reject(501, 'PERSISTENCE_SKIP_NO_GENERIC_CRUD', [req.target.name])
@@ -77,6 +79,19 @@ module.exports = cds.service.impl(function () {
77
79
 
78
80
  let result
79
81
 
82
+ // validate that all elements in path exist on db, if necessary
83
+ // - INSERT has no where clause to do this in one roundtrip
84
+ // - SELECT returns [] -> really empty collection or invalid path?
85
+ let pathExistsQuery
86
+ const { ref } = (req.query.INSERT && req.query.INSERT.into) || (req.query.SELECT && req.query.SELECT.from) || {}
87
+ // REVISIT: why is copy necessary?
88
+ if (ref && ref.length > 1) pathExistsQuery = SELECT(1).from({ ref: deepCopyArray(ref.slice(0, -1)) })
89
+
90
+ if (req.event === 'CREATE' && pathExistsQuery) {
91
+ const res = await pathExistsQuery
92
+ if (res.length === 0) req.reject(404)
93
+ }
94
+
80
95
  // no changes, no op (otherwise, @cds.on.update gets new values), but we need to check existence
81
96
  if (req.event === 'UPDATE' && onlyKeysRemain(req)) {
82
97
  if (await _targetEntityDoesNotExist(req)) req.reject(404)
@@ -95,7 +110,13 @@ module.exports = cds.service.impl(function () {
95
110
  result = await cds.tx(req).run(req.query, req.data)
96
111
  }
97
112
 
98
- if (req.event === 'READ') return result
113
+ if (req.event === 'READ') {
114
+ if ((result == null || result.length === 0) && pathExistsQuery) {
115
+ const res = await pathExistsQuery
116
+ if (res.length === 0) req.reject(404)
117
+ }
118
+ return result
119
+ }
99
120
 
100
121
  if (req.event === 'DELETE') {
101
122
  if (result === 0) req.reject(404)
@@ -55,6 +55,7 @@ NON_WRITABLE_VIEW={0} on views with join and/or union is not supported
55
55
  # db
56
56
  NO_DATABASE_CONNECTION=No database connection
57
57
  ENTITY_ALREADY_EXISTS=Entity already exists
58
+ ENTITY_LOCKED=Entity locked
58
59
  UNIQUE_CONSTRAINT_VIOLATION=Unique constraint violation
59
60
  FK_CONSTRAINT_VIOLATION=Foreign key constraint violation
60
61
 
@@ -72,6 +73,8 @@ ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
72
73
  ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via association "{2}"
73
74
  ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitely exposed as part of the service
74
75
  EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
76
+ EXPAND_COUNT_UNSUPPORTED="$count" is not supported for expand operation
77
+ ORDERBY_LAMBDA_UNSUPPORTED="$orderby" does not support lambda
75
78
 
76
79
  # rest protocol adapter
77
80
  INVALID_RESOURCE="{0}" is not a valid resource
@@ -52,12 +52,8 @@ function targetFromPath(path, model) {
52
52
  ? definitions[current.elements[r.id].target]
53
53
  : definitions[r.id] || definitions[r.id.replace(/_drafts$/, '')]
54
54
  } else {
55
- const next = current.elements[r]
56
- if (next.isAssociation) {
57
- current = definitions[next.target]
58
- } else {
59
- current = next
60
- }
55
+ const next = current ? current.elements[r] : definitions[r]
56
+ current = next.isAssociation ? definitions[next.target] : next
61
57
  }
62
58
  }
63
59
  return current
@@ -13,54 +13,10 @@ const { getPathFromRef, getEntityFromPath } = require('../../common/utils/path')
13
13
  const { addToWhere } = require('../../common/utils/cqn')
14
14
  const { removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
15
15
  const { addRefToWhereIfNecessary } = require('../../../odata/afterburner')
16
+ const { addAliasToExpression, PARENT_ALIAS, FOREIGN_ALIAS } = require('../../db/utils/generateAliases')
16
17
 
17
- const PARENT_ALIAS = '_parent_'
18
- const PARENT_ALIAS_REGEX = new RegExp('^' + PARENT_ALIAS + '\\d*$')
19
- const FOREIGN_ALIAS = '_foreign_'
20
18
  const OPERATIONS = ['=', '>', '<', '!=', '<>', '>=', '<=', 'like', 'between', 'in', 'not in']
21
19
 
22
- // special case in lambda functions
23
- const _addParentAlias = (where, alias) => {
24
- where.forEach(e => {
25
- if (e.ref && e.ref[0].match(PARENT_ALIAS_REGEX)) {
26
- e.ref[0] = alias
27
- }
28
- })
29
- }
30
-
31
- const _addAliasToElement = (e, alias) => {
32
- if (e.ref) {
33
- return { ref: [alias, ...e.ref] }
34
- }
35
-
36
- if (e.list) {
37
- return { list: e.list.map(arg => _addAliasToElement(arg, alias)) }
38
- }
39
-
40
- if (e.func) {
41
- const args = e.args.map(arg => _addAliasToElement(arg, alias))
42
- return { ...e, args }
43
- }
44
-
45
- if (e.SELECT && e.SELECT.where) {
46
- _addParentAlias(e.SELECT.where, alias)
47
- }
48
-
49
- if (e.xpr) {
50
- return { xpr: e.xpr.map(e1 => _addAliasToElement(e1, alias)) }
51
- }
52
-
53
- return e
54
- }
55
-
56
- const _addAliasToExpression = (expression, alias) => {
57
- if (!alias) {
58
- return expression
59
- }
60
-
61
- return expression.map(e => _addAliasToElement(e, alias))
62
- }
63
-
64
20
  const _elementFromRef = (name, entity) => {
65
21
  if (!entity) return
66
22
 
@@ -112,7 +68,7 @@ const convertPathExpressionToWhere = (fromClause, model, options) => {
112
68
  const currentSelect = SELECT.from(`${currentEntityName} as ${tableAlias}`)
113
69
 
114
70
  if (fromClause.ref[i].where) {
115
- currentSelect.where(_addAliasToExpression(fromClause.ref[i].where, tableAlias))
71
+ currentSelect.where(addAliasToExpression(fromClause.ref[i].where, tableAlias))
116
72
  }
117
73
 
118
74
  // REVISIT: Only args in last segment are handled, intermediate ones are ignored
@@ -326,35 +282,15 @@ const _createWindowCQN = (SELECT, model) => {
326
282
  delete SELECT.groupBy
327
283
  }
328
284
 
329
- const _isAny = element => {
330
- return Array.isArray(element.ref) && element.ref.slice(-1)[0].id
285
+ const _unshiftRefsWithNavigation = nav => el => {
286
+ if (el.ref) return { ref: [...nav, ...el.ref] }
287
+ if (el.xpr) return { xpr: el.xpr.map(_unshiftRefsWithNavigation(nav)) }
288
+ return el
331
289
  }
332
290
 
333
- const _isAll = element => {
334
- const last = Array.isArray(element.ref) && element.ref.slice(-1)[0]
335
- return last && last.id && last.where && last.where[0] === 'not' && last.where[1].xpr
336
- }
337
-
338
- // eslint-disable-next-line complexity
339
- const _getWhereExistsSubSelect = (cqn, where, index, model, options) => {
340
- const _unshiftRefsWithNavigation = nav => el => {
341
- if (el.ref) return { ref: [...nav, ...el.ref] }
342
- if (el.xpr) return { xpr: el.xpr.map(_unshiftRefsWithNavigation(nav)) }
343
- return el
344
- }
345
-
346
- if (!options.lambdaIteration) options.lambdaIteration = 1
347
- const outerAlias =
348
- (cqn.SELECT.from.ref && cqn.SELECT.from.as) ||
349
- (cqn.SELECT.from.args && cqn.SELECT.from.args[0].ref && cqn.SELECT.from.args[0].as) ||
350
- PARENT_ALIAS + options.lambdaIteration
351
- const innerAlias = FOREIGN_ALIAS + options.lambdaIteration
352
- cqn.SELECT.from.as = outerAlias
353
- const ref = cqn.SELECT.from.ref || (cqn.SELECT.from.args && cqn.SELECT.from.args[0].ref)
354
- const queryTarget = getEntityFromPath(getPathFromRef(ref), model)
355
-
356
- let nav = where[index].ref.map(el => (el.id ? el.id : el))
357
- const last = where[index].ref.slice(-1)[0]
291
+ const _getWhereExistsSubSelect = (queryTarget, outerAlias, innerAlias, ref, model, options) => {
292
+ let nav = ref.map(el => (el.id ? el.id : el))
293
+ const last = ref.slice(-1)[0]
358
294
  const navName = queryTarget.elements[nav[0]] ? nav[0] : nav[nav.length - 1]
359
295
  const navElement = queryTarget.elements[navName]
360
296
 
@@ -381,22 +317,30 @@ const _getWhereExistsSubSelect = (cqn, where, index, model, options) => {
381
317
  : last.where
382
318
  : undefined
383
319
 
320
+ if (!innerAlias && !outerAlias) {
321
+ innerAlias = navElement.target
322
+ outerAlias = navElement.parent.name
323
+ }
324
+
384
325
  const subSelect = SELECT.from({ ref: [navElement.target], as: innerAlias })
385
326
  if (condition) {
386
327
  if (subSelect.SELECT.from.as) {
387
328
  for (let i = 0; i < condition.length; i++) {
329
+ if (!condition[i].ref) continue
388
330
  if (
389
- condition[i].ref &&
390
331
  condition[i].ref.length > 1 &&
391
332
  condition[i].ref.every(r => typeof r === 'string') &&
392
333
  condition[i].ref[0] === navName
393
334
  ) {
394
335
  condition[i].ref[0] = subSelect.SELECT.from.as
336
+ } else if (condition[i].ref.length === 1) {
337
+ condition[i].ref.unshift(subSelect.SELECT.from.as)
395
338
  }
396
339
  }
397
340
  }
398
341
  // skip for where: [{ val: 1 }]
399
- if (condition.length > 1) subSelect.where(condition)
342
+ // where: [{ func: 'contains', args: [] }] must be evaluated
343
+ if (condition.length > 1 || (condition.length === 1 && !('val' in condition[0]))) subSelect.where(condition)
400
344
  }
401
345
 
402
346
  subSelect.where(queryTarget._relations[navName].join(innerAlias, outerAlias))
@@ -445,42 +389,9 @@ const _ensureExpandedNestedWhereExists = ({ ref }, aliases) => {
445
389
  return { ref: [acc] }
446
390
  }
447
391
 
448
- const _convertWhereExists = (where, cqn, model, options) => {
449
- for (let i = 0; i < where.length; i++) {
450
- const element = where[i]
451
-
452
- // ensure where exists are fully expanded
453
- if (
454
- (element === 'exists' && where[i + 1].ref && where[i > 1 ? i - 1 : 0] !== 'not') ||
455
- (element === 'not' && where[i + 1] === 'exists' && where[i + 2].ref)
456
- ) {
457
- const offset = element === 'not' ? 2 : 1
458
- const aliases = cqn.SELECT.from.as
459
- ? [cqn.SELECT.from.as]
460
- : cqn.SELECT.from.join
461
- ? cqn.SELECT.from.args.map(arg => arg.as)
462
- : []
463
- where[i + offset] = _ensureExpandedNestedWhereExists(where[i + offset], aliases)
464
- }
465
-
466
- if (element.xpr) {
467
- _convertWhereExists(element.xpr) // > recursing into nested {xpr}
468
- } else if (element === 'exists' && _isAny(where[i + 1])) {
469
- where[i + 1] = _getWhereExistsSubSelect(cqn, where, i + 1, model, options)
470
- } else if (element === 'not' && where[i + 1] === 'exists' && _isAll(where[i + 2])) {
471
- where[i + 2] = _getWhereExistsSubSelect(cqn, where, i + 2, model, options)
472
- }
473
- }
474
- }
475
-
476
- const convertWhereExists = (cqn, model, options) => {
477
- if (cqn.SELECT.where) _convertWhereExists(cqn.SELECT.where, cqn, model, options)
478
- }
479
-
480
392
  const _getRefIndex = (where, index) => {
481
393
  if (
482
394
  where[index - 1].ref &&
483
- // REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called there
484
395
  where[index - 1].ref[where[index - 1].ref.length - 1] !== 'InProcessByUser' &&
485
396
  where[index + 1].val !== undefined &&
486
397
  where[index + 1].val !== null &&
@@ -490,7 +401,6 @@ const _getRefIndex = (where, index) => {
490
401
  }
491
402
  if (
492
403
  where[index + 1].ref &&
493
- // REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called there
494
404
  where[index + 1].ref[where[index + 1].ref.length - 1] !== 'InProcessByUser' &&
495
405
  where[index - 1].val !== undefined &&
496
406
  where[index - 1].val !== null &&
@@ -500,6 +410,77 @@ const _getRefIndex = (where, index) => {
500
410
  }
501
411
  }
502
412
 
413
+ const _convertWhereExistsColumn = (column, model, options, queryTarget) => {
414
+ const lambdaIteration = options.lambdaIteration
415
+ if (typeof column === 'object') {
416
+ if (column.SELECT) {
417
+ convertWhereExists(column.SELECT, model, options)
418
+ options.lambdaIteration = lambdaIteration
419
+ } else if (column.where) {
420
+ convertWhereExists(column, model, options, queryTarget)
421
+ options.lambdaIteration = lambdaIteration
422
+ }
423
+ }
424
+ }
425
+
426
+ // eslint-disable-next-line complexity
427
+ const convertWhereExists = (query, model, options, currentTarget) => {
428
+ const { where, columns, expand } = query
429
+ let innerAlias, outerAlias, queryTarget
430
+ const { ref, as } =
431
+ (query.ref && query) ||
432
+ (query.from &&
433
+ ((query.from.ref && query.from) || (query.from.args && query.from.args[0].ref && query.from.args[0]))) ||
434
+ {}
435
+ if (!ref) return
436
+
437
+ if (!options.lambdaIteration) options.lambdaIteration = 1
438
+ const lambdaIteration = options.lambdaIteration
439
+ if (!currentTarget) {
440
+ queryTarget = getEntityFromPath(getPathFromRef(ref), model)
441
+ outerAlias = as || PARENT_ALIAS + lambdaIteration
442
+ innerAlias = FOREIGN_ALIAS + lambdaIteration
443
+ } else {
444
+ const element = currentTarget.elements[ref]
445
+ if (element && element.isAssociation) {
446
+ queryTarget = element._target
447
+ }
448
+ }
449
+
450
+ if (where) {
451
+ for (let i = 0; i < where.length; i++) {
452
+ const element = where[i]
453
+
454
+ // ensure where exists are fully expanded
455
+ if (element === 'exists' && where[i + 1].ref) {
456
+ const ref = query.from || query
457
+ const aliases = ref.as ? [ref.as] : ref.join ? ref.args.map(arg => arg.as) : []
458
+ where[i + 1] = _ensureExpandedNestedWhereExists(where[i + 1], aliases)
459
+ }
460
+
461
+ if (element.xpr) {
462
+ convertWhereExists({ ...query, where: element.xpr }, model, options) // > recursing into nested {xpr}
463
+ } else if (element === 'exists' && where[i + 1].ref) {
464
+ if (query.from) {
465
+ query.from.as = outerAlias
466
+ }
467
+ where[i + 1] = _getWhereExistsSubSelect(queryTarget, outerAlias, innerAlias, where[i + 1].ref, model, options)
468
+ i += 1
469
+ } else if (element.SELECT) {
470
+ convertWhereExists(element.SELECT, model, options)
471
+ }
472
+ options.lambdaIteration = lambdaIteration
473
+ }
474
+ }
475
+
476
+ if (columns) {
477
+ columns.forEach(column => _convertWhereExistsColumn(column, model, options, queryTarget))
478
+ }
479
+ if (expand && expand !== '*') {
480
+ expand.forEach(column => _convertWhereExistsColumn(column, model, options, queryTarget))
481
+ }
482
+ }
483
+
503
484
  const _convertNotEqual = (container, partName = 'where') => {
504
485
  const where = container[partName]
505
486
 
@@ -667,7 +648,7 @@ const _convertPathExpression = (SELECT, model, options = {}) => {
667
648
  if (alias) {
668
649
  SELECT.from.as = alias
669
650
  if (SELECT.where && alias !== prevAlias) {
670
- SELECT.where = _addAliasToExpression(SELECT.where, alias)
651
+ SELECT.where = addAliasToExpression(SELECT.where, alias)
671
652
  }
672
653
  }
673
654
  if (columns) {
@@ -690,7 +671,7 @@ const _convertPathExpression = (SELECT, model, options = {}) => {
690
671
  }
691
672
  // TODO: REVISIT: We need to add alias to subselect in .where, .columns, .from, ... etc
692
673
  if (where) {
693
- if (options.isDraft) {
674
+ if (options._4fiori) {
694
675
  addToWhere({ SELECT }, where)
695
676
  } else {
696
677
  addToWhere({ SELECT }, removeIsActiveEntityRecursively(where))
@@ -702,8 +683,7 @@ const _convertPathExpression = (SELECT, model, options = {}) => {
702
683
  const _convertSelect = (query, model, _options) => {
703
684
  const options = Object.assign(
704
685
  {
705
- isDB: _options.service instanceof cds.DatabaseService,
706
- isDraft: _options.draft,
686
+ _4db: _options.service instanceof cds.DatabaseService,
707
687
  isStreaming: query._streaming
708
688
  },
709
689
  _options
@@ -719,7 +699,7 @@ const _convertSelect = (query, model, _options) => {
719
699
  if (cds.env.features.odata_new_parser) _flattenCQN(query)
720
700
 
721
701
  // lambda functions
722
- convertWhereExists(query, model, options)
702
+ convertWhereExists(query.SELECT, model, options)
723
703
 
724
704
  // add 'or is null' in case of '!='
725
705
  if (query.SELECT._4odata) {
@@ -728,7 +708,7 @@ const _convertSelect = (query, model, _options) => {
728
708
  }
729
709
 
730
710
  _convertPathExpression(query.SELECT, model, options)
731
- rewriteAsterisks(query, model, options.isDB, options.isDraft)
711
+ rewriteAsterisks(query, model, options)
732
712
  if (query.SELECT.where && _isCountNavigation(query.SELECT.where)) {
733
713
  _convertCountNavigation(query.SELECT, model)
734
714
  }
@@ -779,12 +759,6 @@ const _convertInsert = (query, model, options) => {
779
759
 
780
760
  const resolvedView = resolveView(insert, model, cds.db)
781
761
 
782
- if (query.INSERT.into.ref && query.INSERT.into.ref.length > 1) {
783
- const copyFrom = [...query.INSERT.into.ref]
784
- copyFrom.pop()
785
- resolvedView._validationQuery = _convertSelect(SELECT.from({ ref: copyFrom }).columns([1]), model, options)
786
- }
787
-
788
762
  return resolvedView
789
763
  }
790
764
 
@@ -830,7 +804,7 @@ const _convertDelete = (query, model, options) => {
830
804
 
831
805
  if (alias) deleet.DELETE.from = { ref: [target], as: alias }
832
806
  if (where) deleet.where(where)
833
- if (query.DELETE.where) deleet.where(_addAliasToExpression(query.DELETE.where, alias))
807
+ if (query.DELETE.where) deleet.where(addAliasToExpression(query.DELETE.where, alias))
834
808
 
835
809
  const targetEntity = model.definitions[target]
836
810
  if (!targetEntity) return deleet
@@ -867,7 +841,7 @@ const _convertUpdate = (query, model, options) => {
867
841
 
868
842
  if (alias) update.UPDATE.entity = { ref: [target], as: alias }
869
843
  if (where) update.where(where)
870
- if (query.UPDATE.where) update.where(_addAliasToExpression(query.UPDATE.where, alias))
844
+ if (query.UPDATE.where) update.where(addAliasToExpression(query.UPDATE.where, alias))
871
845
 
872
846
  const targetEntity = model.definitions[target]
873
847
  if (!targetEntity) return update
@@ -6,17 +6,27 @@ const getEtagElement = entity => {
6
6
  return Object.values(entity.elements).find(element => element['@odata.etag'])
7
7
  }
8
8
 
9
- const _getUps = (entity, model) => {
9
+ const getComp2oneParents = (entity, model) => {
10
+ if (!entity) return []
11
+ if (entity.own('__comp2oneParents')) return entity.__comp2oneParents
12
+ const invalidationFn = element => !(element.is2one && element._isCompositionEffective)
13
+ const comp2oneParents = _getUps(entity, model, invalidationFn)
14
+ return entity.set('__comp2oneParents', comp2oneParents)
15
+ }
16
+
17
+ const _getUps = (entity, model, invalidationFn) => {
18
+ if (entity.own('__parents')) return entity.__parents
10
19
  const ups = []
11
20
  for (const def of Object.values(model.definitions)) {
12
21
  if (def.kind !== 'entity' || !def.associations) continue
13
22
  for (const element of Object.values(def.associations)) {
14
23
  if (element.target !== entity.name || element._isBacklink) continue
15
24
  if (element.name === 'SiblingEntity') continue
25
+ if (invalidationFn && invalidationFn(element)) continue
16
26
  ups.push(element)
17
27
  }
18
28
  }
19
- return ups
29
+ return entity.set('__parents', ups)
20
30
  }
21
31
 
22
32
  const _ifDataSubject = (entity, role) => {
@@ -189,5 +199,6 @@ module.exports = {
189
199
  getElementDeep,
190
200
  isRootEntity,
191
201
  getDataSubject,
192
- alias2ref
202
+ alias2ref,
203
+ getComp2oneParents
193
204
  }
@@ -183,7 +183,24 @@ const _resolvedKeys = (foreignKeys, targetKeys, fillChild) => {
183
183
 
184
184
  for (let i = 0; i < foreignKeys.length; i++) {
185
185
  const fk = foreignKeys[i]
186
- const tk = targetKeys[i]
186
+ let tk
187
+
188
+ /*
189
+ * REVISIT: poor man's look-up of target key with fallback to targetKeys[i]
190
+ * Look at elements, then try to find it in query and resolve recursively until you have the full path.
191
+ * Once you have the full path, you can find it in the target entity.
192
+ * NOTE: There can be projections upon projections and renamings in every projection. -> not yet covered!!!
193
+ */
194
+ const tkCol =
195
+ targetKeys[i].parent.query &&
196
+ targetKeys[i].parent.query.SELECT.columns &&
197
+ targetKeys[i].parent.query.SELECT.columns.find(
198
+ c => c.ref && `${fk['@odata.foreignKey4']}_${c.ref.join('_')}` === fk.name
199
+ )
200
+ tk = tkCol && targetKeys.find(tk => tk.name === (tkCol.as ? tkCol.as : tkCol.ref.join('_')))
201
+ // with composition of aspects, the lookup fails -> we need this final fallback
202
+ if (!tk) tk = targetKeys[i]
203
+
187
204
  if (fk._isStructured) {
188
205
  i += _resolve4struct(targetKeys, fk, foreignKeyPropagations, fillChild, false, i)
189
206
  } else if (tk._isStructured) {
@@ -1,4 +1,5 @@
1
1
  const { where2obj } = require('./cqn')
2
+ const { deepCopyArray } = require('./copy')
2
3
 
3
4
  function _getOnCondElements(onCond, onCondElements = []) {
4
5
  const andIndex = onCond.indexOf('and')
@@ -17,7 +18,7 @@ function _getOnCondElements(onCond, onCondElements = []) {
17
18
  function _modifyWhereWithNavigations(where, newWhere, targetKeyElement, keyName) {
18
19
  if (where) {
19
20
  // copy where else query will be modified
20
- const whereCopy = JSON.parse(JSON.stringify(where))
21
+ const whereCopy = deepCopyArray(where)
21
22
  if (newWhere.length > 0) newWhere.push('and')
22
23
  newWhere.push(...whereCopy)
23
24
  }
@@ -24,7 +24,7 @@ const getEntityFromPath = (path, model) => {
24
24
  while (segments.length) {
25
25
  const segment = segments.shift()
26
26
  current = current.elements[segment.id || segment]
27
- if (current.target) current = model.definitions[current.target]
27
+ if (current && current.target) current = model.definitions[current.target]
28
28
  }
29
29
  return current
30
30
  }