@sap/cds 5.6.3 → 5.7.3

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 (187) hide show
  1. package/CHANGELOG.md +135 -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 +4 -2
  38. package/lib/log/index.js +2 -2
  39. package/lib/ql/Whereable.js +1 -0
  40. package/lib/req/cds-context.js +79 -0
  41. package/lib/req/context.js +5 -77
  42. package/lib/req/request.js +1 -1
  43. package/lib/serve/Service-api.js +8 -4
  44. package/lib/serve/Service-dispatch.js +0 -7
  45. package/lib/serve/Service-methods.js +6 -8
  46. package/lib/serve/Transaction.js +35 -30
  47. package/lib/serve/adapters.js +1 -4
  48. package/lib/utils/axios.js +1 -1
  49. package/libx/_runtime/audit/Service.js +44 -20
  50. package/libx/_runtime/audit/generic/personal/access.js +16 -11
  51. package/libx/_runtime/audit/generic/personal/modification.js +5 -5
  52. package/libx/_runtime/audit/generic/personal/utils.js +46 -37
  53. package/libx/_runtime/{common/auth → auth}/index.js +21 -7
  54. package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
  55. package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
  56. package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
  57. package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
  58. package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
  59. package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
  60. package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
  62. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
  63. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -69
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +29 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +7 -1
  74. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +9 -5
  75. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  76. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  77. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  78. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
  79. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  80. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  81. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  84. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  85. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  86. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
  87. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  88. package/libx/_runtime/cds-services/services/Service.js +0 -6
  89. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  90. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
  91. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  92. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  93. package/libx/_runtime/cds-services/util/assert.js +1 -262
  94. package/libx/_runtime/cds.js +6 -9
  95. package/libx/_runtime/common/aspects/entity.js +1 -1
  96. package/libx/_runtime/common/composition/delete.js +4 -2
  97. package/libx/_runtime/common/composition/update.js +22 -38
  98. package/libx/_runtime/common/composition/utils.js +3 -7
  99. package/libx/_runtime/common/error/standardError.js +11 -0
  100. package/libx/_runtime/common/generic/auth.js +63 -33
  101. package/libx/_runtime/common/generic/crud.js +11 -23
  102. package/libx/_runtime/common/generic/input.js +20 -0
  103. package/libx/_runtime/common/generic/paging.js +2 -2
  104. package/libx/_runtime/common/generic/put.js +4 -10
  105. package/libx/_runtime/common/generic/sorting.js +12 -30
  106. package/libx/_runtime/common/i18n/messages.properties +2 -0
  107. package/libx/_runtime/common/perf/index.js +24 -0
  108. package/libx/_runtime/common/utils/cqn.js +58 -1
  109. package/libx/_runtime/common/utils/cqn2cqn4sql.js +298 -121
  110. package/libx/_runtime/common/utils/csn.js +38 -56
  111. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  112. package/libx/_runtime/common/utils/resolveView.js +4 -5
  113. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  114. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  115. package/libx/_runtime/common/utils/structured.js +35 -25
  116. package/libx/_runtime/db/Service.js +0 -6
  117. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  118. package/libx/_runtime/db/expand/expandCQNToJoin.js +38 -52
  119. package/libx/_runtime/db/expand/index.js +3 -1
  120. package/libx/_runtime/db/generic/arrayed.js +3 -1
  121. package/libx/_runtime/db/generic/input.js +52 -10
  122. package/libx/_runtime/db/generic/integrity.js +367 -26
  123. package/libx/_runtime/db/generic/virtual.js +51 -13
  124. package/libx/_runtime/db/query/read.js +12 -8
  125. package/libx/_runtime/db/query/update.js +9 -3
  126. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  127. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  128. package/libx/_runtime/fiori/generic/activate.js +1 -0
  129. package/libx/_runtime/fiori/generic/before.js +2 -1
  130. package/libx/_runtime/fiori/generic/edit.js +1 -0
  131. package/libx/_runtime/fiori/generic/patch.js +1 -1
  132. package/libx/_runtime/fiori/generic/read.js +128 -57
  133. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
  134. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  135. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
  136. package/libx/_runtime/fiori/utils/delete.js +7 -1
  137. package/libx/_runtime/hana/Service.js +1 -8
  138. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  139. package/libx/_runtime/hana/execute.js +10 -4
  140. package/libx/_runtime/hana/pool.js +55 -45
  141. package/libx/_runtime/hana/search.js +7 -6
  142. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  143. package/libx/_runtime/hana/searchToContains.js +3 -1
  144. package/libx/_runtime/index.js +5 -5
  145. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  146. package/libx/_runtime/messaging/Outbox.js +53 -0
  147. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  148. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  149. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  150. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  151. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  152. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  153. package/libx/_runtime/messaging/file-based.js +5 -5
  154. package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
  155. package/libx/_runtime/messaging/message-queuing.js +2 -3
  156. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  157. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  158. package/libx/_runtime/messaging/service.js +16 -30
  159. package/libx/_runtime/remote/Service.js +15 -0
  160. package/libx/_runtime/remote/utils/client.js +15 -3
  161. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  162. package/libx/_runtime/sqlite/Service.js +7 -10
  163. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  164. package/libx/_runtime/sqlite/execute.js +18 -12
  165. package/libx/_runtime/types/api.js +2 -1
  166. package/libx/gql/resolvers/parse/ast2cqn/columns.js +1 -1
  167. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
  168. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  169. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +222 -130
  170. package/libx/odata/index.js +23 -14
  171. package/libx/odata/parser.js +1 -0
  172. package/libx/odata/utils.js +57 -0
  173. package/libx/rest/RestAdapter.js +2 -6
  174. package/libx/rest/utils/data.js +1 -6
  175. package/package.json +4 -3
  176. package/server.js +4 -5
  177. package/srv/audit-log.cds +87 -0
  178. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  179. package/srv/flex.js +1 -0
  180. package/srv/outbox.cds +11 -0
  181. package/srv/outbox.js +0 -0
  182. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  183. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  184. package/libx/odata/odata2cqn/index.js +0 -3
  185. package/libx/odata/odata2cqn/parser.js +0 -1
  186. package/libx/odata/readme.md +0 -1
  187. 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,11 @@ 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
+ // where: [{ func: 'contains', args: [] }] must be evaluated
400
+ if (condition.length > 1 || (condition.length === 1 && !('val' in condition[0]))) subSelect.where(condition)
294
401
  }
402
+
295
403
  subSelect.where(queryTarget._relations[navName].join(innerAlias, outerAlias))
296
404
  if (cds.env.effective.odata.structs) {
297
405
  flattenStructuredSelect(subSelect, model)
@@ -300,25 +408,76 @@ const _getLambdaSubSelect = (cqn, where, index, lambdaOp, model, options) => {
300
408
 
301
409
  // nested where exists needs recursive conversion
302
410
  options.lambdaIteration++
411
+
303
412
  return _convertSelect(subSelect, model, options)
304
413
  }
305
414
 
306
- const _convertLambda = (cqn, model, options) => {
307
- const where = cqn.SELECT.where
308
- if (where) _recurse(where)
309
- function _recurse(where) {
310
- where.forEach((element, index) => {
311
- if (element.xpr) {
312
- _recurse(element.xpr) // recursing into nested {xpr}
313
- } else if (element === 'exists' && _isAny(where[index + 1])) {
314
- where[index + 1] = _getLambdaSubSelect(cqn, where, index + 1, 'any', model, options)
315
- } else if (element === 'not' && where[index + 1] === 'exists' && _isAll(where[index + 2])) {
316
- where[index + 2] = _getLambdaSubSelect(cqn, where, index + 2, 'all', model, options)
415
+ /*
416
+ * ensures that the where exists are fully expanded and have an infix filter
417
+ * example: where: ['exists', { ref: ['book', 'author'] }]
418
+ * -> where: ['exists', { id: 'book', where: ['exists', { id: 'author', where: [{ val: 1 }] }] }]
419
+ */
420
+ const _ensureExpandedNestedWhereExists = ({ ref }, aliases) => {
421
+ const acc = {}
422
+ let cur = acc
423
+ let alias
424
+ for (const r of ref) {
425
+ if (Array.isArray(cur)) {
426
+ if (cur.length > 1) {
427
+ // > element already has condition(s)
428
+ cur.push('and', 'exists', { ref: [{}] })
429
+ cur = cur[cur.length - 1].ref[0]
430
+ } else {
431
+ cur.unshift('exists')
432
+ cur[1] = { ref: [{}] }
433
+ cur = cur[1].ref[0]
317
434
  }
318
- })
435
+ }
436
+ if (typeof r === 'string' && aliases.includes(r)) {
437
+ alias = r
438
+ } else {
439
+ // use Object.assign to preserve pointers
440
+ if (typeof r === 'string') Object.assign(cur, { id: r, where: [{ val: 1 }] })
441
+ else Object.assign(cur, r)
442
+ cur = cur.where
443
+ }
444
+ }
445
+ if (alias) return { ref: [alias, acc] }
446
+ return { ref: [acc] }
447
+ }
448
+
449
+ const _convertWhereExists = (where, cqn, model, options) => {
450
+ for (let i = 0; i < where.length; i++) {
451
+ const element = where[i]
452
+
453
+ // ensure where exists are fully expanded
454
+ if (
455
+ (element === 'exists' && where[i + 1].ref && where[i > 1 ? i - 1 : 0] !== 'not') ||
456
+ (element === 'not' && where[i + 1] === 'exists' && where[i + 2].ref)
457
+ ) {
458
+ const offset = element === 'not' ? 2 : 1
459
+ const aliases = cqn.SELECT.from.as
460
+ ? [cqn.SELECT.from.as]
461
+ : cqn.SELECT.from.join
462
+ ? cqn.SELECT.from.args.map(arg => arg.as)
463
+ : []
464
+ where[i + offset] = _ensureExpandedNestedWhereExists(where[i + offset], aliases)
465
+ }
466
+
467
+ if (element.xpr) {
468
+ _convertWhereExists(element.xpr) // > recursing into nested {xpr}
469
+ } else if (element === 'exists' && _isAny(where[i + 1])) {
470
+ where[i + 1] = _getWhereExistsSubSelect(cqn, where, i + 1, model, options)
471
+ } else if (element === 'not' && where[i + 1] === 'exists' && _isAll(where[i + 2])) {
472
+ where[i + 2] = _getWhereExistsSubSelect(cqn, where, i + 2, model, options)
473
+ }
319
474
  }
320
475
  }
321
476
 
477
+ const convertWhereExists = (cqn, model, options) => {
478
+ if (cqn.SELECT.where) _convertWhereExists(cqn.SELECT.where, cqn, model, options)
479
+ }
480
+
322
481
  const _getRefIndex = (where, index) => {
323
482
  if (
324
483
  where[index - 1].ref &&
@@ -342,8 +501,8 @@ const _getRefIndex = (where, index) => {
342
501
  }
343
502
  }
344
503
 
345
- const _convertNotEqual = container => {
346
- const where = container.where
504
+ const _convertNotEqual = (container, partName = 'where') => {
505
+ const where = container[partName]
347
506
 
348
507
  if (where) {
349
508
  let changed
@@ -360,13 +519,13 @@ const _convertNotEqual = container => {
360
519
  }
361
520
 
362
521
  if (el && el.SELECT) {
363
- _convertNotEqual(el.SELECT)
522
+ _convertNotEqual(el.SELECT, partName)
364
523
  }
365
524
  })
366
525
 
367
526
  // delete undefined values
368
527
  if (changed) {
369
- container.where = where.filter(el => el)
528
+ container[partName] = where.filter(el => el)
370
529
  }
371
530
  }
372
531
 
@@ -374,9 +533,9 @@ const _convertNotEqual = container => {
374
533
  container.columns.forEach(col => {
375
534
  if (typeof col === 'object') {
376
535
  if (col.SELECT) {
377
- _convertNotEqual(col.SELECT)
536
+ _convertNotEqual(col.SELECT, partName)
378
537
  } else if (col.where) {
379
- _convertNotEqual(col)
538
+ _convertNotEqual(col, partName)
380
539
  }
381
540
  }
382
541
  })
@@ -384,7 +543,7 @@ const _convertNotEqual = container => {
384
543
  }
385
544
 
386
545
  const _ifOrderByOrWhereSkip = (queryTarget, ref, model) => {
387
- if (!queryTarget.elements[ref[0]]) return
546
+ if (!queryTarget || !queryTarget.elements[ref[0]]) return
388
547
  return ref.some(el => {
389
548
  if (typeof el !== 'string') return
390
549
  const orderbyTarget = queryTarget.elements[el]._isStructured
@@ -398,9 +557,12 @@ const _ifOrderByOrWhereSkip = (queryTarget, ref, model) => {
398
557
  })
399
558
  }
400
559
 
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)
560
+ const _skip = (queryTarget, ref, alias, model) => {
561
+ // we remove the alias, if it exists
562
+ const _ref = ref[0] === alias ? ref.slice(1) : ref
563
+ if (_ref.length === 1) return queryTarget && queryTarget.elements[_ref[0]] && queryTarget.elements[_ref[0]].virtual
564
+
565
+ return _ifOrderByOrWhereSkip(queryTarget, _ref, model)
404
566
  }
405
567
 
406
568
  const _convertOrderByIfSkip = (orderByCQN, index) => {
@@ -412,21 +574,22 @@ const _convertWhereIfSkip = (whereCQN, index) => {
412
574
  OPERATIONS.includes(whereCQN[index + 1]) ? whereCQN.splice(index + 1, 2) : whereCQN.splice(index - 2, 2)
413
575
  }
414
576
 
415
- const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, processFn) => {
577
+ const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, alias, processFn) => {
416
578
  const queryTarget = model.definitions[ensureNoDraftsSuffix(target)]
417
579
 
418
580
  orderByOrWhereCQN.forEach((el, index) => {
419
- if (el.ref && _skip(queryTarget, el.ref, model)) processFn(orderByOrWhereCQN, index)
581
+ if (el.ref && _skip(queryTarget, el.ref, alias, model)) processFn(orderByOrWhereCQN, index)
420
582
  })
421
583
  }
422
584
 
423
585
  const _convertOrderByOrWhereIfSkip = (cqn, target, model) => {
586
+ const alias = cqn.SELECT.from.as
424
587
  if (cqn.SELECT.orderBy && cqn.SELECT.orderBy.length > 1) {
425
- _convertOrderByOrWhereCQN(cqn.SELECT.orderBy, target, model, _convertOrderByIfSkip)
588
+ _convertOrderByOrWhereCQN(cqn.SELECT.orderBy, target, model, alias, _convertOrderByIfSkip)
426
589
  }
427
590
 
428
591
  if (cqn.SELECT.where) {
429
- _convertOrderByOrWhereCQN(cqn.SELECT.where, target, model, _convertWhereIfSkip)
592
+ _convertOrderByOrWhereCQN(cqn.SELECT.where, target, model, alias, _convertWhereIfSkip)
430
593
  }
431
594
  }
432
595
 
@@ -449,7 +612,7 @@ const _convertExpand = expand => {
449
612
  const _convertRefWhereInExpand = columns => {
450
613
  if (columns) {
451
614
  columns.forEach(col => {
452
- if (col.expand) {
615
+ if (col.expand && typeof col.expand !== 'string') {
453
616
  _convertExpand(col.expand)
454
617
  }
455
618
  })
@@ -486,102 +649,115 @@ const _flattenXpr = cqn => {
486
649
  cqn.forEach(_flattenCQN)
487
650
  }
488
651
 
652
+ const _convertPathExpression = (SELECT, model, options = {}) => {
653
+ const prevAlias = SELECT.from.as
654
+ for (const whereEl of SELECT.where || []) {
655
+ if (typeof whereEl === 'object' && whereEl.SELECT) _convertPathExpression(whereEl.SELECT, model)
656
+ }
657
+ const conversion = convertPathExpressionToWhere(SELECT.from, model, options)
658
+ if (!conversion) return
659
+ const { target, alias, where, cardinality, columns, args } = conversion
660
+
661
+ // REVISIT: It's not possible to just change the reference (i.e. SELECT.from.ref = [target])
662
+ // as many parts of our code base still refer to SELECT.from (e.g. authorization)
663
+ if (args) {
664
+ SELECT.from = { ref: [{ id: target, args }] }
665
+ } else {
666
+ SELECT.from = { ref: [target] }
667
+ }
668
+ if (alias) {
669
+ SELECT.from.as = alias
670
+ if (SELECT.where && alias !== prevAlias) {
671
+ SELECT.where = _addAliasToExpression(SELECT.where, alias)
672
+ }
673
+ }
674
+ if (columns) {
675
+ // TODO: use streaming as outer property
676
+ if (options.isStreaming) SELECT.columns = columns
677
+ else {
678
+ if (!SELECT.columns) {
679
+ // Okra always wants to have the key values, remove once we relax this requirement
680
+ if (model.definitions[target] && model.definitions[target].keys) {
681
+ SELECT.columns = Object.keys(model.definitions[target].keys)
682
+ .filter(k => !model.definitions[target].keys[k].isAssociation)
683
+ .map(k => ({ ref: [k] }))
684
+ } else SELECT.columns = []
685
+ }
686
+ SELECT.columns.push(...columns)
687
+ }
688
+ }
689
+ if (cardinality && cardinality.max === 1) {
690
+ SELECT.one = true
691
+ }
692
+ // TODO: REVISIT: We need to add alias to subselect in .where, .columns, .from, ... etc
693
+ if (where) {
694
+ if (options.isDraft) {
695
+ addToWhere({ SELECT }, where)
696
+ } else {
697
+ addToWhere({ SELECT }, removeIsActiveEntityRecursively(where))
698
+ }
699
+ }
700
+ }
701
+
489
702
  // eslint-disable-next-line complexity
490
- const _convertSelect = (query, model, options) => {
491
- const isDB = options.service instanceof cds.DatabaseService
492
- const isDraft = options.draft
703
+ const _convertSelect = (query, model, _options) => {
704
+ const options = Object.assign(
705
+ {
706
+ isDB: _options.service instanceof cds.DatabaseService,
707
+ isDraft: _options.draft,
708
+ isStreaming: query._streaming
709
+ },
710
+ _options
711
+ )
712
+ // ensure query is ql enabled
713
+ if (!(query instanceof Query)) Object.setPrototypeOf(query, Object.getPrototypeOf(SELECT()))
714
+ if (query.SELECT.from && query.SELECT.from.SELECT) {
715
+ if (query.SELECT._4odata) query.SELECT.from.SELECT._4odata = true
716
+ query.SELECT.from = _convertSelect(query.SELECT.from, model, options)
717
+ }
718
+
493
719
  // REVISIT: a temporary workaround for xpr from new parser
494
720
  if (cds.env.features.odata_new_parser) _flattenCQN(query)
495
721
 
496
722
  // lambda functions
497
- _convertLambda(query, model, options)
723
+ convertWhereExists(query, model, options)
498
724
 
499
725
  // 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
726
+ if (query.SELECT._4odata) {
727
+ _convertNotEqual(query.SELECT, 'where')
728
+ _convertNotEqual(query.SELECT, 'having')
528
729
  }
529
730
 
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)
731
+ _convertPathExpression(query.SELECT, model, options)
732
+ rewriteAsterisks(query, model, options.isDB, options.isDraft)
733
+ if (query.SELECT.where && _isCountNavigation(query.SELECT.where)) {
734
+ _convertCountNavigation(query.SELECT, model)
536
735
  }
537
736
 
538
737
  // extract where clause if it is in column expand ref
539
738
  _convertRefWhereInExpand(query.SELECT.columns)
540
739
 
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)
545
-
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
- }
740
+ // REVISIT: The following operations only work for _one_ entity.
741
+ // We must also enable them for joins etc.
742
+ const { entityName, alias } = getEntityNameFromCQN(query)
558
743
 
559
- if (cardinality && cardinality.max === 1) {
560
- query.SELECT.one = true
561
- }
744
+ // remove virtual and with skip annotated fields in orderby and where
745
+ _convertOrderByOrWhereIfSkip(query, entityName, model)
562
746
 
563
- if (query.SELECT.search) {
564
- searchOptions = { ...searchOptions, ...{ targetName: target } }
565
- search2cqn4sql(query, model, searchOptions)
747
+ if (query.SELECT.search && !options.suppressSearch) {
748
+ search2cqn4sql(query, model, { ...query._searchOptions, ...{ entityName, alias } })
566
749
  }
567
750
 
568
- if (query.SELECT.where) {
569
- select.where(_addAliasToExpression(query.SELECT.where, select.SELECT.from.as))
751
+ if (query.SELECT.columns && cds.env.effective.odata.structs) {
752
+ flattenStructuredSelect(query, model)
570
753
  }
571
754
 
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)
755
+ // topcount with groupby
756
+ if (query.SELECT.columns && _isBottomTop(query.SELECT.columns)) {
757
+ _createWindowCQN(query.SELECT, model)
582
758
  }
583
759
 
584
- return select
760
+ return query
585
761
  }
586
762
 
587
763
  const _convertInsert = (query, model, options) => {
@@ -732,5 +908,6 @@ const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
732
908
 
733
909
  module.exports = {
734
910
  cqn2cqn4sql,
911
+ convertWhereExists,
735
912
  convertPathExpressionToWhere
736
913
  }