@sap/cds 5.6.4 → 5.7.1

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 (176) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/_i18n/i18n_fr.properties +4 -4
  3. package/apis/cds.d.ts +7 -10
  4. package/apis/connect.d.ts +3 -3
  5. package/apis/core.d.ts +2 -4
  6. package/apis/models.d.ts +2 -3
  7. package/apis/ql.d.ts +0 -1
  8. package/apis/services.d.ts +3 -3
  9. package/bin/build/buildTaskFactory.js +16 -10
  10. package/bin/build/buildTaskProviderFactory.js +3 -3
  11. package/bin/build/constants.js +2 -1
  12. package/bin/build/provider/buildTaskProviderInternal.js +14 -14
  13. package/bin/build/provider/hana/2migration.js +2 -3
  14. package/bin/build/provider/hana/index.js +34 -0
  15. package/bin/build/provider/hana/migrationtable.js +90 -22
  16. package/bin/build/provider/hana/template/undeploy.json +5 -0
  17. package/bin/build/provider/node-cf/index.js +9 -2
  18. package/bin/serve.js +16 -18
  19. package/lib/compile/cdsc.js +15 -5
  20. package/lib/compile/etc/_localized.js +4 -4
  21. package/lib/compile/extend.js +8 -0
  22. package/lib/compile/index.js +3 -1
  23. package/lib/compile/minify.js +61 -0
  24. package/lib/compile/resolve.js +4 -1
  25. package/lib/compile/to/gql.js +9 -0
  26. package/lib/compile/to/sql.js +26 -30
  27. package/lib/connect/index.js +1 -1
  28. package/lib/core/entities.js +0 -3
  29. package/lib/core/infer.js +1 -0
  30. package/lib/core/reflect.js +0 -34
  31. package/lib/deploy.js +25 -17
  32. package/lib/env/defaults.js +3 -1
  33. package/lib/env/index.js +8 -3
  34. package/lib/env/presets.js +38 -0
  35. package/lib/env/requires.js +16 -11
  36. package/lib/index.js +13 -11
  37. package/lib/log/format/kibana.js +3 -1
  38. package/lib/log/index.js +2 -2
  39. package/lib/req/cds-context.js +79 -0
  40. package/lib/req/context.js +5 -77
  41. package/lib/req/request.js +1 -1
  42. package/lib/serve/Service-api.js +8 -4
  43. package/lib/serve/Service-dispatch.js +0 -7
  44. package/lib/serve/Service-methods.js +6 -8
  45. package/lib/serve/Transaction.js +35 -30
  46. package/lib/serve/adapters.js +1 -4
  47. package/lib/utils/axios.js +1 -1
  48. package/libx/_runtime/audit/Service.js +44 -20
  49. package/libx/_runtime/audit/generic/personal/access.js +16 -11
  50. package/libx/_runtime/audit/generic/personal/modification.js +5 -5
  51. package/libx/_runtime/audit/generic/personal/utils.js +46 -37
  52. package/libx/_runtime/{common/auth → auth}/index.js +21 -7
  53. package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
  54. package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
  55. package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
  56. package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
  57. package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
  58. package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
  59. package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
  60. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
  62. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
  71. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +24 -0
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  73. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  74. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  75. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
  76. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  77. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  78. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  79. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  80. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  81. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  84. package/libx/_runtime/cds-services/services/Service.js +0 -6
  85. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  86. package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -6
  87. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  88. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  89. package/libx/_runtime/cds-services/util/assert.js +1 -262
  90. package/libx/_runtime/cds.js +6 -9
  91. package/libx/_runtime/common/aspects/entity.js +1 -1
  92. package/libx/_runtime/common/composition/delete.js +4 -2
  93. package/libx/_runtime/common/composition/update.js +22 -38
  94. package/libx/_runtime/common/composition/utils.js +3 -7
  95. package/libx/_runtime/common/error/standardError.js +11 -0
  96. package/libx/_runtime/common/generic/auth.js +61 -30
  97. package/libx/_runtime/common/generic/crud.js +11 -23
  98. package/libx/_runtime/common/generic/input.js +20 -0
  99. package/libx/_runtime/common/generic/put.js +4 -10
  100. package/libx/_runtime/common/generic/sorting.js +12 -30
  101. package/libx/_runtime/common/perf/index.js +24 -0
  102. package/libx/_runtime/common/utils/cqn.js +58 -1
  103. package/libx/_runtime/common/utils/cqn2cqn4sql.js +289 -114
  104. package/libx/_runtime/common/utils/csn.js +38 -56
  105. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  106. package/libx/_runtime/common/utils/resolveView.js +4 -5
  107. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  108. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  109. package/libx/_runtime/common/utils/structured.js +35 -25
  110. package/libx/_runtime/db/Service.js +0 -6
  111. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  112. package/libx/_runtime/db/expand/expandCQNToJoin.js +38 -52
  113. package/libx/_runtime/db/expand/index.js +3 -1
  114. package/libx/_runtime/db/generic/input.js +52 -10
  115. package/libx/_runtime/db/generic/integrity.js +367 -26
  116. package/libx/_runtime/db/generic/virtual.js +51 -13
  117. package/libx/_runtime/db/query/update.js +9 -3
  118. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  119. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  120. package/libx/_runtime/fiori/generic/activate.js +1 -0
  121. package/libx/_runtime/fiori/generic/before.js +2 -1
  122. package/libx/_runtime/fiori/generic/edit.js +1 -0
  123. package/libx/_runtime/fiori/generic/patch.js +1 -1
  124. package/libx/_runtime/fiori/generic/read.js +123 -57
  125. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  126. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +3 -3
  127. package/libx/_runtime/fiori/utils/delete.js +7 -1
  128. package/libx/_runtime/hana/Service.js +1 -8
  129. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  130. package/libx/_runtime/hana/execute.js +10 -4
  131. package/libx/_runtime/hana/pool.js +55 -45
  132. package/libx/_runtime/hana/search.js +7 -6
  133. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  134. package/libx/_runtime/hana/searchToContains.js +3 -1
  135. package/libx/_runtime/index.js +5 -5
  136. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  137. package/libx/_runtime/messaging/Outbox.js +53 -0
  138. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  139. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  140. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  141. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  142. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  143. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  144. package/libx/_runtime/messaging/file-based.js +5 -5
  145. package/libx/_runtime/messaging/message-queuing.js +2 -3
  146. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  147. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  148. package/libx/_runtime/messaging/service.js +16 -30
  149. package/libx/_runtime/remote/Service.js +15 -0
  150. package/libx/_runtime/remote/utils/client.js +15 -3
  151. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  152. package/libx/_runtime/sqlite/Service.js +7 -10
  153. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  154. package/libx/_runtime/sqlite/execute.js +18 -12
  155. package/libx/_runtime/types/api.js +2 -1
  156. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +19 -15
  157. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  158. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +171 -130
  159. package/libx/odata/index.js +16 -14
  160. package/libx/odata/parser.js +1 -0
  161. package/libx/odata/utils.js +57 -0
  162. package/libx/rest/RestAdapter.js +2 -6
  163. package/libx/rest/utils/data.js +1 -6
  164. package/package.json +4 -3
  165. package/server.js +4 -5
  166. package/srv/audit-log.cds +87 -0
  167. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  168. package/srv/flex.js +1 -0
  169. package/srv/outbox.cds +11 -0
  170. package/srv/outbox.js +0 -0
  171. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  172. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  173. package/libx/odata/odata2cqn/index.js +0 -3
  174. package/libx/odata/odata2cqn/parser.js +0 -1
  175. package/libx/odata/readme.md +0 -1
  176. package/libx/odata/utils/index.js +0 -64
@@ -1,5 +1,6 @@
1
1
  const cds = require('../../cds')
2
2
  const { SELECT, INSERT, DELETE, UPDATE } = cds.ql
3
+ const Query = require('../../../../lib/ql/Query')
3
4
 
4
5
  const { resolveView } = require('./resolveView')
5
6
  const { ensureNoDraftsSuffix } = require('./draft')
@@ -9,7 +10,9 @@ const { getEntityNameFromCQN } = require('./entityFromCqn')
9
10
  const getError = require('../../common/error')
10
11
  const { rewriteAsterisks } = require('./rewriteAsterisks')
11
12
  const { getPathFromRef, getEntityFromPath } = require('../../common/utils/path')
13
+ const { addToWhere } = require('../../common/utils/cqn')
12
14
  const { removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
15
+ const { addRefToWhereIfNecessary } = require('../../../odata/afterburner')
13
16
 
14
17
  const PARENT_ALIAS = '_parent_'
15
18
  const PARENT_ALIAS_REGEX = new RegExp('^' + PARENT_ALIAS + '\\d*$')
@@ -76,14 +79,15 @@ const _getEntityName = (fromClause, entity, i) => {
76
79
  }
77
80
 
78
81
  const convertPathExpressionToWhere = (fromClause, model, options) => {
82
+ if (!fromClause.ref) return
79
83
  if (fromClause.ref.length === 1) {
80
84
  const target = _getTargetFromRef(fromClause.ref[0])
81
85
  const alias = fromClause.as
82
- const { where, cardinality } = fromClause.ref[0]
83
- return { target, alias, where, cardinality }
86
+ const { where, cardinality, args } = fromClause.ref[0]
87
+ return { target, alias, where, cardinality, args }
84
88
  }
85
89
 
86
- let previousSelect, previousEntityName, previousTableAlias, structParent
90
+ let previousSelect, previousEntityName, previousTableAlias, structParent, previousArgs
87
91
  let prefix = []
88
92
  let columns
89
93
  for (let i = 0; i < fromClause.ref.length; i++) {
@@ -111,6 +115,9 @@ const convertPathExpressionToWhere = (fromClause, model, options) => {
111
115
  currentSelect.where(_addAliasToExpression(fromClause.ref[i].where, tableAlias))
112
116
  }
113
117
 
118
+ // REVISIT: Only args in last segment are handled, intermediate ones are ignored
119
+ const currentArgs = fromClause.ref[i].args
120
+
114
121
  if (i !== fromClause.ref.length - 1) {
115
122
  currentSelect.columns([1])
116
123
  }
@@ -129,12 +136,14 @@ const convertPathExpressionToWhere = (fromClause, model, options) => {
129
136
  previousTableAlias = tableAlias
130
137
  previousSelect = currentSelect
131
138
  previousEntityName = currentEntityName
139
+ previousArgs = currentArgs
132
140
  }
133
141
 
134
142
  return {
135
143
  target: previousEntityName,
136
144
  alias: previousTableAlias,
137
145
  where: previousSelect && previousSelect.SELECT && previousSelect.SELECT.where,
146
+ args: previousArgs,
138
147
  columns
139
148
  }
140
149
  }
@@ -179,24 +188,31 @@ const _getOrderByForWindowFn = bottomTop => {
179
188
 
180
189
  const _getWindowXpr = (groupBy, bottomTop) => {
181
190
  const xpr = [{ func: 'ROW_NUMBER', args: [] }, 'OVER', '(']
182
- xpr.push(
183
- 'PARTITION BY',
184
- ...groupBy.reduce((acc, el, i) => {
185
- if (i < groupBy.length - 1) {
186
- acc.push(el)
187
- acc.push(',')
188
- } else {
189
- acc.push(el)
190
- }
191
+ if (groupBy)
192
+ xpr.push(
193
+ 'PARTITION BY',
194
+ ...groupBy.reduce((acc, el, i) => {
195
+ if (i < groupBy.length - 1) {
196
+ acc.push(el)
197
+ acc.push(',')
198
+ } else {
199
+ acc.push(el)
200
+ }
191
201
 
192
- return acc
193
- }, [])
194
- )
202
+ return acc
203
+ }, [])
204
+ )
195
205
  xpr.push('ORDER BY', _getOrderByForWindowFn(bottomTop))
196
206
  xpr.push(')')
197
207
  return { xpr: xpr, as: 'rowNumber' }
198
208
  }
199
209
 
210
+ const _isNavCountFunc = el => el.func && el.func === 'count' && el.args[0] !== '*'
211
+
212
+ const _isCountNavigation = columns => {
213
+ return columns.some(_isNavCountFunc)
214
+ }
215
+
200
216
  const _isBottomTop = columns => {
201
217
  return columns.some(el => el.func && (el.func === 'topcount' || el.func === 'bottomcount'))
202
218
  }
@@ -205,6 +221,89 @@ const _getWindColumns = (columns, groupBy, bottomTop) => {
205
221
  return [].concat(columns, _getWindowXpr(groupBy, bottomTop))
206
222
  }
207
223
 
224
+ const _convertCountNavigation = (SELECT, model) => {
225
+ const entityName = SELECT.from.ref[0].id || SELECT.from.ref[0]
226
+ const entity = model.definitions[entityName]
227
+
228
+ const newWhere = []
229
+ for (let i = 0; i < SELECT.where.length; i++) {
230
+ const element = SELECT.where[i]
231
+
232
+ if (!element.func) {
233
+ newWhere.push(element)
234
+ continue
235
+ }
236
+
237
+ const navigations = element.args[0].ref
238
+ const navigationName = navigations[0].id || navigations[0]
239
+ if (entity.elements[navigationName]) {
240
+ let currentEntity = entity
241
+ let topQuery
242
+ let lastQuery
243
+
244
+ for (let j = 0; j < navigations.length; j++) {
245
+ if (navigations[j].where) {
246
+ addRefToWhereIfNecessary(navigations[j].where, currentEntity)
247
+ }
248
+
249
+ const nextNavigation = navigations[j]
250
+ const nestedExistsQuery = _buildExistsQuery(
251
+ currentEntity,
252
+ nextNavigation,
253
+ j > 0 ? `ALIAS_${j - 1}` : currentEntity.name,
254
+ `ALIAS_${j}`
255
+ )
256
+ if (!topQuery) {
257
+ topQuery = nestedExistsQuery
258
+ }
259
+
260
+ const nestedNavigationName = nextNavigation.id || nextNavigation
261
+ if (j === navigations.length - 1) {
262
+ nestedExistsQuery.columns('count(1) as counted')
263
+ if (lastQuery) {
264
+ lastQuery.where([nestedExistsQuery, SELECT.where[i + 1], SELECT.where[i + 2]])
265
+ newWhere.push('exists', topQuery)
266
+ } else {
267
+ newWhere.push(nestedExistsQuery, SELECT.where[i + 1], SELECT.where[i + 2])
268
+ }
269
+ } else {
270
+ if (lastQuery) {
271
+ lastQuery.where('exists', nestedExistsQuery)
272
+ }
273
+ }
274
+ lastQuery = nestedExistsQuery
275
+ currentEntity = currentEntity.elements[nestedNavigationName]._target
276
+ }
277
+ i += 2
278
+ }
279
+ }
280
+ SELECT.where = newWhere
281
+ }
282
+
283
+ const _addTableName = (where, tableName) => {
284
+ return where.map(ref => {
285
+ if (ref.ref) {
286
+ ref.ref.unshift(tableName)
287
+ }
288
+ return ref
289
+ })
290
+ }
291
+
292
+ const _buildExistsQuery = (entity, navigation, currentAlias, nextAlias) => {
293
+ const target = entity.elements[navigation.id || navigation].target
294
+
295
+ const existsQuery = cds.ql.SELECT.from(target)
296
+ existsQuery.SELECT.from.as = nextAlias
297
+
298
+ if (navigation && navigation.where) {
299
+ existsQuery.where(_addTableName(navigation.where, nextAlias))
300
+ }
301
+
302
+ const onCond = entity.elements[navigation.id || navigation]._relations.join(nextAlias, currentAlias)
303
+
304
+ return existsQuery.where(onCond)
305
+ }
306
+
208
307
  const _createWindowCQN = (SELECT, model) => {
209
308
  const bottomTop = SELECT.columns.filter(el => _isBottomTop([el]))
210
309
  const columns = (SELECT.columns = [])
@@ -237,7 +336,7 @@ const _isAll = element => {
237
336
  }
238
337
 
239
338
  // eslint-disable-next-line complexity
240
- const _getLambdaSubSelect = (cqn, where, index, lambdaOp, model, options) => {
339
+ const _getWhereExistsSubSelect = (cqn, where, index, model, options) => {
241
340
  const _unshiftRefsWithNavigation = nav => el => {
242
341
  if (el.ref) return { ref: [...nav, ...el.ref] }
243
342
  if (el.xpr) return { xpr: el.xpr.map(_unshiftRefsWithNavigation(nav)) }
@@ -254,10 +353,11 @@ const _getLambdaSubSelect = (cqn, where, index, lambdaOp, model, options) => {
254
353
  const ref = cqn.SELECT.from.ref || (cqn.SELECT.from.args && cqn.SELECT.from.args[0].ref)
255
354
  const queryTarget = getEntityFromPath(getPathFromRef(ref), model)
256
355
 
257
- const nav = where[index].ref.map(el => (el.id ? el.id : el))
356
+ let nav = where[index].ref.map(el => (el.id ? el.id : el))
258
357
  const last = where[index].ref.slice(-1)[0]
259
358
  const navName = queryTarget.elements[nav[0]] ? nav[0] : nav[nav.length - 1]
260
359
  const navElement = queryTarget.elements[navName]
360
+
261
361
  const lastElement = nav.reduce((csn, segment) => {
262
362
  if (csn.items) return csn // arrayed not supported
263
363
  if (csn.elements) {
@@ -267,9 +367,14 @@ const _getLambdaSubSelect = (cqn, where, index, lambdaOp, model, options) => {
267
367
  return next
268
368
  }
269
369
  }, queryTarget)
270
- if (lastElement.items)
271
- /* arrayed */
370
+
371
+ if (lastElement.items) {
372
+ // > arrayed
272
373
  throw getError(501, `Condition expression with arrayed elements is not supported`)
374
+ }
375
+
376
+ if (nav[0] === outerAlias) nav = nav.slice(1)
377
+
273
378
  const condition = last.where
274
379
  ? nav.length > 1
275
380
  ? last.where.map(_unshiftRefsWithNavigation(nav.slice(1)))
@@ -290,8 +395,10 @@ const _getLambdaSubSelect = (cqn, where, index, lambdaOp, model, options) => {
290
395
  }
291
396
  }
292
397
  }
293
- subSelect.where(condition)
398
+ // skip for where: [{ val: 1 }]
399
+ if (condition.length > 1) subSelect.where(condition)
294
400
  }
401
+
295
402
  subSelect.where(queryTarget._relations[navName].join(innerAlias, outerAlias))
296
403
  if (cds.env.effective.odata.structs) {
297
404
  flattenStructuredSelect(subSelect, model)
@@ -300,23 +407,73 @@ const _getLambdaSubSelect = (cqn, where, index, lambdaOp, model, options) => {
300
407
 
301
408
  // nested where exists needs recursive conversion
302
409
  options.lambdaIteration++
410
+
303
411
  return _convertSelect(subSelect, model, options)
304
412
  }
305
413
 
306
- const _convertLambda = (cqn, model, options) => {
307
- const where = cqn.SELECT.where
308
- if (where) _recurse(where)
414
+ /*
415
+ * ensures that the where exists are fully expanded and have an infix filter
416
+ * example: where: ['exists', { ref: ['book', 'author'] }]
417
+ * -> where: ['exists', { id: 'book', where: ['exists', { id: 'author', where: [{ val: 1 }] }] }]
418
+ */
419
+ const _ensureExpandedNestedWhereExists = ({ ref }, aliases) => {
420
+ const acc = {}
421
+ let cur = acc
422
+ let alias
423
+ for (const r of ref) {
424
+ if (Array.isArray(cur)) {
425
+ if (cur.length > 1) {
426
+ // > element already has condition(s)
427
+ cur.push('and', 'exists', { ref: [{}] })
428
+ cur = cur[cur.length - 1].ref[0]
429
+ } else {
430
+ cur.unshift('exists')
431
+ cur[1] = { ref: [{}] }
432
+ cur = cur[1].ref[0]
433
+ }
434
+ }
435
+ if (typeof r === 'string' && aliases.includes(r)) {
436
+ alias = r
437
+ } else {
438
+ // use Object.assign to preserve pointers
439
+ if (typeof r === 'string') Object.assign(cur, { id: r, where: [{ val: 1 }] })
440
+ else Object.assign(cur, r)
441
+ cur = cur.where
442
+ }
443
+ }
444
+ if (alias) return { ref: [alias, acc] }
445
+ return { ref: [acc] }
446
+ }
447
+
448
+ const convertWhereExists = (cqn, model, options) => {
309
449
  function _recurse(where) {
310
450
  where.forEach((element, index) => {
451
+ // ensure where exists are fully expanded
452
+ if (where && where.some(ele => ele === 'exists')) {
453
+ where.forEach((element, index) => {
454
+ if (element === 'exists' && where[index + 1].ref) {
455
+ const aliases = cqn.SELECT.from.as
456
+ ? [cqn.SELECT.from.as]
457
+ : cqn.SELECT.from.join
458
+ ? cqn.SELECT.from.args.map(arg => arg.as)
459
+ : []
460
+ where[index + 1] = _ensureExpandedNestedWhereExists(where[index + 1], aliases)
461
+ }
462
+ })
463
+ }
464
+
311
465
  if (element.xpr) {
312
466
  _recurse(element.xpr) // recursing into nested {xpr}
313
467
  } else if (element === 'exists' && _isAny(where[index + 1])) {
314
- where[index + 1] = _getLambdaSubSelect(cqn, where, index + 1, 'any', model, options)
468
+ where[index + 1] = _getWhereExistsSubSelect(cqn, where, index + 1, model, options)
315
469
  } else if (element === 'not' && where[index + 1] === 'exists' && _isAll(where[index + 2])) {
316
- where[index + 2] = _getLambdaSubSelect(cqn, where, index + 2, 'all', model, options)
470
+ where[index + 2] = _getWhereExistsSubSelect(cqn, where, index + 2, model, options)
317
471
  }
318
472
  })
319
473
  }
474
+
475
+ const where = cqn.SELECT.where
476
+ if (where) _recurse(where)
320
477
  }
321
478
 
322
479
  const _getRefIndex = (where, index) => {
@@ -342,8 +499,8 @@ const _getRefIndex = (where, index) => {
342
499
  }
343
500
  }
344
501
 
345
- const _convertNotEqual = container => {
346
- const where = container.where
502
+ const _convertNotEqual = (container, partName = 'where') => {
503
+ const where = container[partName]
347
504
 
348
505
  if (where) {
349
506
  let changed
@@ -360,13 +517,13 @@ const _convertNotEqual = container => {
360
517
  }
361
518
 
362
519
  if (el && el.SELECT) {
363
- _convertNotEqual(el.SELECT)
520
+ _convertNotEqual(el.SELECT, partName)
364
521
  }
365
522
  })
366
523
 
367
524
  // delete undefined values
368
525
  if (changed) {
369
- container.where = where.filter(el => el)
526
+ container[partName] = where.filter(el => el)
370
527
  }
371
528
  }
372
529
 
@@ -374,9 +531,9 @@ const _convertNotEqual = container => {
374
531
  container.columns.forEach(col => {
375
532
  if (typeof col === 'object') {
376
533
  if (col.SELECT) {
377
- _convertNotEqual(col.SELECT)
534
+ _convertNotEqual(col.SELECT, partName)
378
535
  } else if (col.where) {
379
- _convertNotEqual(col)
536
+ _convertNotEqual(col, partName)
380
537
  }
381
538
  }
382
539
  })
@@ -384,7 +541,7 @@ const _convertNotEqual = container => {
384
541
  }
385
542
 
386
543
  const _ifOrderByOrWhereSkip = (queryTarget, ref, model) => {
387
- if (!queryTarget.elements[ref[0]]) return
544
+ if (!queryTarget || !queryTarget.elements[ref[0]]) return
388
545
  return ref.some(el => {
389
546
  if (typeof el !== 'string') return
390
547
  const orderbyTarget = queryTarget.elements[el]._isStructured
@@ -398,9 +555,12 @@ const _ifOrderByOrWhereSkip = (queryTarget, ref, model) => {
398
555
  })
399
556
  }
400
557
 
401
- const _skip = (queryTarget, ref, model) => {
402
- if (ref.length === 1) return queryTarget && queryTarget.elements[ref[0]] && queryTarget.elements[ref[0]].virtual
403
- return _ifOrderByOrWhereSkip(queryTarget, ref, model)
558
+ const _skip = (queryTarget, ref, alias, model) => {
559
+ // we remove the alias, if it exists
560
+ const _ref = ref[0] === alias ? ref.slice(1) : ref
561
+ if (_ref.length === 1) return queryTarget && queryTarget.elements[_ref[0]] && queryTarget.elements[_ref[0]].virtual
562
+
563
+ return _ifOrderByOrWhereSkip(queryTarget, _ref, model)
404
564
  }
405
565
 
406
566
  const _convertOrderByIfSkip = (orderByCQN, index) => {
@@ -412,21 +572,22 @@ const _convertWhereIfSkip = (whereCQN, index) => {
412
572
  OPERATIONS.includes(whereCQN[index + 1]) ? whereCQN.splice(index + 1, 2) : whereCQN.splice(index - 2, 2)
413
573
  }
414
574
 
415
- const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, processFn) => {
575
+ const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, alias, processFn) => {
416
576
  const queryTarget = model.definitions[ensureNoDraftsSuffix(target)]
417
577
 
418
578
  orderByOrWhereCQN.forEach((el, index) => {
419
- if (el.ref && _skip(queryTarget, el.ref, model)) processFn(orderByOrWhereCQN, index)
579
+ if (el.ref && _skip(queryTarget, el.ref, alias, model)) processFn(orderByOrWhereCQN, index)
420
580
  })
421
581
  }
422
582
 
423
583
  const _convertOrderByOrWhereIfSkip = (cqn, target, model) => {
584
+ const alias = cqn.SELECT.from.as
424
585
  if (cqn.SELECT.orderBy && cqn.SELECT.orderBy.length > 1) {
425
- _convertOrderByOrWhereCQN(cqn.SELECT.orderBy, target, model, _convertOrderByIfSkip)
586
+ _convertOrderByOrWhereCQN(cqn.SELECT.orderBy, target, model, alias, _convertOrderByIfSkip)
426
587
  }
427
588
 
428
589
  if (cqn.SELECT.where) {
429
- _convertOrderByOrWhereCQN(cqn.SELECT.where, target, model, _convertWhereIfSkip)
590
+ _convertOrderByOrWhereCQN(cqn.SELECT.where, target, model, alias, _convertWhereIfSkip)
430
591
  }
431
592
  }
432
593
 
@@ -449,7 +610,7 @@ const _convertExpand = expand => {
449
610
  const _convertRefWhereInExpand = columns => {
450
611
  if (columns) {
451
612
  columns.forEach(col => {
452
- if (col.expand) {
613
+ if (col.expand && typeof col.expand !== 'string') {
453
614
  _convertExpand(col.expand)
454
615
  }
455
616
  })
@@ -486,102 +647,115 @@ const _flattenXpr = cqn => {
486
647
  cqn.forEach(_flattenCQN)
487
648
  }
488
649
 
650
+ const _convertPathExpression = (SELECT, model, options = {}) => {
651
+ const prevAlias = SELECT.from.as
652
+ for (const whereEl of SELECT.where || []) {
653
+ if (typeof whereEl === 'object' && whereEl.SELECT) _convertPathExpression(whereEl.SELECT, model)
654
+ }
655
+ const conversion = convertPathExpressionToWhere(SELECT.from, model, options)
656
+ if (!conversion) return
657
+ const { target, alias, where, cardinality, columns, args } = conversion
658
+
659
+ // REVISIT: It's not possible to just change the reference (i.e. SELECT.from.ref = [target])
660
+ // as many parts of our code base still refer to SELECT.from (e.g. authorization)
661
+ if (args) {
662
+ SELECT.from = { ref: [{ id: target, args }] }
663
+ } else {
664
+ SELECT.from = { ref: [target] }
665
+ }
666
+ if (alias) {
667
+ SELECT.from.as = alias
668
+ if (SELECT.where && alias !== prevAlias) {
669
+ SELECT.where = _addAliasToExpression(SELECT.where, alias)
670
+ }
671
+ }
672
+ if (columns) {
673
+ // TODO: use streaming as outer property
674
+ if (options.isStreaming) SELECT.columns = columns
675
+ else {
676
+ if (!SELECT.columns) {
677
+ // Okra always wants to have the key values, remove once we relax this requirement
678
+ if (model.definitions[target] && model.definitions[target].keys) {
679
+ SELECT.columns = Object.keys(model.definitions[target].keys)
680
+ .filter(k => !model.definitions[target].keys[k].isAssociation)
681
+ .map(k => ({ ref: [k] }))
682
+ } else SELECT.columns = []
683
+ }
684
+ SELECT.columns.push(...columns)
685
+ }
686
+ }
687
+ if (cardinality && cardinality.max === 1) {
688
+ SELECT.one = true
689
+ }
690
+ // TODO: REVISIT: We need to add alias to subselect in .where, .columns, .from, ... etc
691
+ if (where) {
692
+ if (options.isDraft) {
693
+ addToWhere({ SELECT }, where)
694
+ } else {
695
+ addToWhere({ SELECT }, removeIsActiveEntityRecursively(where))
696
+ }
697
+ }
698
+ }
699
+
489
700
  // eslint-disable-next-line complexity
490
- const _convertSelect = (query, model, options) => {
491
- const isDB = options.service instanceof cds.DatabaseService
492
- const isDraft = options.draft
701
+ const _convertSelect = (query, model, _options) => {
702
+ const options = Object.assign(
703
+ {
704
+ isDB: _options.service instanceof cds.DatabaseService,
705
+ isDraft: _options.draft,
706
+ isStreaming: query._streaming
707
+ },
708
+ _options
709
+ )
710
+ // ensure query is ql enabled
711
+ if (!(query instanceof Query)) Object.setPrototypeOf(query, Object.getPrototypeOf(SELECT()))
712
+ if (query.SELECT.from && query.SELECT.from.SELECT) {
713
+ if (query.SELECT._4odata) query.SELECT.from.SELECT._4odata = true
714
+ query.SELECT.from = _convertSelect(query.SELECT.from, model, options)
715
+ }
716
+
493
717
  // REVISIT: a temporary workaround for xpr from new parser
494
718
  if (cds.env.features.odata_new_parser) _flattenCQN(query)
495
719
 
496
720
  // lambda functions
497
- _convertLambda(query, model, options)
721
+ convertWhereExists(query, model, options)
498
722
 
499
723
  // add 'or is null' in case of '!='
500
- if (query.SELECT._4odata) _convertNotEqual(query.SELECT)
501
-
502
- let searchOptions = query._searchOptions
503
- const cqnSelectFromRef = query.SELECT.from.ref
504
-
505
- // no path expression
506
- if (!cqnSelectFromRef || (cqnSelectFromRef.length === 1 && !cqnSelectFromRef[0].where)) {
507
- rewriteAsterisks(query, cqnSelectFromRef && model.definitions[cqnSelectFromRef[0]], isDB, isDraft)
508
- // extract where clause if it is in column expand ref
509
- _convertRefWhereInExpand(query.SELECT.columns)
510
- // remove virtual and with skip annotated fields in orderby and where
511
- _convertOrderByOrWhereIfSkip(query, getEntityNameFromCQN(query), model)
512
-
513
- if (cqnSelectFromRef && query.SELECT.search && !options.suppressSearch) {
514
- searchOptions = { ...searchOptions, ...{ targetName: cqnSelectFromRef[0] } }
515
- search2cqn4sql(query, model, searchOptions)
516
- }
517
-
518
- if (query.SELECT.columns && cds.env.effective.odata.structs) {
519
- flattenStructuredSelect(query, model)
520
- }
521
-
522
- // topcount with groupby
523
- if (query.SELECT.columns && _isBottomTop(query.SELECT.columns)) {
524
- _createWindowCQN(query.SELECT, model)
525
- }
526
-
527
- return query
724
+ if (query.SELECT._4odata) {
725
+ _convertNotEqual(query.SELECT, 'where')
726
+ _convertNotEqual(query.SELECT, 'having')
528
727
  }
529
728
 
530
- // path expression handling
531
- const { target, alias, where, cardinality, columns } = convertPathExpressionToWhere(query.SELECT.from, model, options)
532
- rewriteAsterisks(query, model.definitions[target], isDB, isDraft, !!columns)
533
- if (columns) {
534
- if (query._streaming) query.SELECT.columns = columns
535
- else query.SELECT.columns.push(...columns)
729
+ _convertPathExpression(query.SELECT, model, options)
730
+ rewriteAsterisks(query, model, options.isDB, options.isDraft)
731
+ if (query.SELECT.where && _isCountNavigation(query.SELECT.where)) {
732
+ _convertCountNavigation(query.SELECT, model)
536
733
  }
537
734
 
538
735
  // extract where clause if it is in column expand ref
539
736
  _convertRefWhereInExpand(query.SELECT.columns)
540
737
 
541
- // remove virtual and with skip annotated fields in orderby and where
542
- _convertOrderByOrWhereIfSkip(query, target, model)
543
-
544
- const select = SELECT.from(target, query.SELECT.columns)
738
+ // REVISIT: The following operations only work for _one_ entity.
739
+ // We must also enable them for joins etc.
740
+ const { entityName, alias } = getEntityNameFromCQN(query)
545
741
 
546
- if (alias) {
547
- select.SELECT.from.as = alias
548
- }
549
-
550
- // TODO: REVISIT: We need to add alias to subselect in .where, .columns, .from, ... etc
551
- if (where) {
552
- if (!isDraft) {
553
- select.where(removeIsActiveEntityRecursively(where))
554
- } else {
555
- select.where(where)
556
- }
557
- }
558
-
559
- if (cardinality && cardinality.max === 1) {
560
- query.SELECT.one = true
561
- }
742
+ // remove virtual and with skip annotated fields in orderby and where
743
+ _convertOrderByOrWhereIfSkip(query, entityName, model)
562
744
 
563
- if (query.SELECT.search) {
564
- searchOptions = { ...searchOptions, ...{ targetName: target } }
565
- search2cqn4sql(query, model, searchOptions)
745
+ if (query.SELECT.search && !options.suppressSearch) {
746
+ search2cqn4sql(query, model, { ...query._searchOptions, ...{ entityName, alias } })
566
747
  }
567
748
 
568
- if (query.SELECT.where) {
569
- select.where(_addAliasToExpression(query.SELECT.where, select.SELECT.from.as))
749
+ if (query.SELECT.columns && cds.env.effective.odata.structs) {
750
+ flattenStructuredSelect(query, model)
570
751
  }
571
752
 
572
- // We add all previous properties ot the newly created query.
573
- // Reason is to not lose the query API functionality
574
- Object.assign(select.SELECT, query.SELECT, {
575
- columns: select.SELECT.columns,
576
- from: select.SELECT.from,
577
- where: select.SELECT.where
578
- })
579
-
580
- if (select.SELECT.columns && cds.env.effective.odata.structs) {
581
- flattenStructuredSelect(select, model)
753
+ // topcount with groupby
754
+ if (query.SELECT.columns && _isBottomTop(query.SELECT.columns)) {
755
+ _createWindowCQN(query.SELECT, model)
582
756
  }
583
757
 
584
- return select
758
+ return query
585
759
  }
586
760
 
587
761
  const _convertInsert = (query, model, options) => {
@@ -732,5 +906,6 @@ const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
732
906
 
733
907
  module.exports = {
734
908
  cqn2cqn4sql,
909
+ convertWhereExists,
735
910
  convertPathExpressionToWhere
736
911
  }