@sap/cds 5.6.2 → 5.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +133 -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 +7 -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 +13 -4
  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 -65
  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 +26 -0
  73. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
  74. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  75. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  76. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  77. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
  78. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  79. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  80. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  81. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  84. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  85. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
  86. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  87. package/libx/_runtime/cds-services/services/Service.js +0 -6
  88. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  89. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
  90. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  91. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  92. package/libx/_runtime/cds-services/util/assert.js +1 -262
  93. package/libx/_runtime/cds.js +6 -9
  94. package/libx/_runtime/common/aspects/entity.js +1 -1
  95. package/libx/_runtime/common/composition/delete.js +4 -2
  96. package/libx/_runtime/common/composition/update.js +22 -35
  97. package/libx/_runtime/common/composition/utils.js +3 -7
  98. package/libx/_runtime/common/error/standardError.js +11 -0
  99. package/libx/_runtime/common/generic/auth.js +63 -33
  100. package/libx/_runtime/common/generic/crud.js +11 -23
  101. package/libx/_runtime/common/generic/input.js +20 -0
  102. package/libx/_runtime/common/generic/paging.js +2 -2
  103. package/libx/_runtime/common/generic/put.js +4 -10
  104. package/libx/_runtime/common/generic/sorting.js +12 -30
  105. package/libx/_runtime/common/perf/index.js +24 -0
  106. package/libx/_runtime/common/utils/cqn.js +58 -1
  107. package/libx/_runtime/common/utils/cqn2cqn4sql.js +297 -121
  108. package/libx/_runtime/common/utils/csn.js +38 -56
  109. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  110. package/libx/_runtime/common/utils/resolveView.js +4 -5
  111. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  112. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  113. package/libx/_runtime/common/utils/structured.js +35 -25
  114. package/libx/_runtime/db/Service.js +0 -6
  115. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  116. package/libx/_runtime/db/expand/expandCQNToJoin.js +38 -52
  117. package/libx/_runtime/db/expand/index.js +3 -1
  118. package/libx/_runtime/db/generic/arrayed.js +3 -1
  119. package/libx/_runtime/db/generic/input.js +52 -10
  120. package/libx/_runtime/db/generic/integrity.js +367 -26
  121. package/libx/_runtime/db/generic/virtual.js +51 -13
  122. package/libx/_runtime/db/query/update.js +9 -3
  123. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  124. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  125. package/libx/_runtime/fiori/generic/activate.js +1 -0
  126. package/libx/_runtime/fiori/generic/before.js +2 -1
  127. package/libx/_runtime/fiori/generic/edit.js +1 -0
  128. package/libx/_runtime/fiori/generic/patch.js +1 -1
  129. package/libx/_runtime/fiori/generic/read.js +155 -57
  130. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
  131. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  132. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
  133. package/libx/_runtime/fiori/utils/delete.js +7 -1
  134. package/libx/_runtime/hana/Service.js +1 -8
  135. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  136. package/libx/_runtime/hana/execute.js +10 -4
  137. package/libx/_runtime/hana/pool.js +55 -45
  138. package/libx/_runtime/hana/search.js +7 -6
  139. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  140. package/libx/_runtime/hana/searchToContains.js +3 -1
  141. package/libx/_runtime/index.js +5 -5
  142. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  143. package/libx/_runtime/messaging/Outbox.js +53 -0
  144. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  145. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  146. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  147. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  148. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  149. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  150. package/libx/_runtime/messaging/file-based.js +5 -5
  151. package/libx/_runtime/messaging/message-queuing.js +2 -3
  152. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  153. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  154. package/libx/_runtime/messaging/service.js +16 -30
  155. package/libx/_runtime/remote/Service.js +15 -0
  156. package/libx/_runtime/remote/utils/client.js +15 -3
  157. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  158. package/libx/_runtime/sqlite/Service.js +7 -10
  159. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  160. package/libx/_runtime/sqlite/execute.js +18 -12
  161. package/libx/_runtime/types/api.js +2 -1
  162. package/libx/gql/resolvers/parse/ast2cqn/columns.js +1 -1
  163. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
  164. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  165. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +182 -118
  166. package/libx/odata/index.js +18 -15
  167. package/libx/odata/parser.js +1 -0
  168. package/libx/odata/utils.js +57 -0
  169. package/libx/rest/RestAdapter.js +2 -6
  170. package/libx/rest/utils/data.js +1 -6
  171. package/package.json +4 -3
  172. package/server.js +4 -5
  173. package/srv/audit-log.cds +87 -0
  174. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  175. package/srv/flex.js +1 -0
  176. package/srv/outbox.cds +11 -0
  177. package/srv/outbox.js +0 -0
  178. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  179. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  180. package/libx/odata/odata2cqn/index.js +0 -3
  181. package/libx/odata/odata2cqn/parser.js +0 -1
  182. package/libx/odata/readme.md +0 -1
  183. 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,25 +407,76 @@ 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)
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)
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]
317
433
  }
318
- })
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 = (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
+ }
319
473
  }
320
474
  }
321
475
 
476
+ const convertWhereExists = (cqn, model, options) => {
477
+ if (cqn.SELECT.where) _convertWhereExists(cqn.SELECT.where, cqn, model, options)
478
+ }
479
+
322
480
  const _getRefIndex = (where, index) => {
323
481
  if (
324
482
  where[index - 1].ref &&
@@ -342,8 +500,8 @@ const _getRefIndex = (where, index) => {
342
500
  }
343
501
  }
344
502
 
345
- const _convertNotEqual = container => {
346
- const where = container.where
503
+ const _convertNotEqual = (container, partName = 'where') => {
504
+ const where = container[partName]
347
505
 
348
506
  if (where) {
349
507
  let changed
@@ -360,13 +518,13 @@ const _convertNotEqual = container => {
360
518
  }
361
519
 
362
520
  if (el && el.SELECT) {
363
- _convertNotEqual(el.SELECT)
521
+ _convertNotEqual(el.SELECT, partName)
364
522
  }
365
523
  })
366
524
 
367
525
  // delete undefined values
368
526
  if (changed) {
369
- container.where = where.filter(el => el)
527
+ container[partName] = where.filter(el => el)
370
528
  }
371
529
  }
372
530
 
@@ -374,9 +532,9 @@ const _convertNotEqual = container => {
374
532
  container.columns.forEach(col => {
375
533
  if (typeof col === 'object') {
376
534
  if (col.SELECT) {
377
- _convertNotEqual(col.SELECT)
535
+ _convertNotEqual(col.SELECT, partName)
378
536
  } else if (col.where) {
379
- _convertNotEqual(col)
537
+ _convertNotEqual(col, partName)
380
538
  }
381
539
  }
382
540
  })
@@ -384,7 +542,7 @@ const _convertNotEqual = container => {
384
542
  }
385
543
 
386
544
  const _ifOrderByOrWhereSkip = (queryTarget, ref, model) => {
387
- if (!queryTarget.elements[ref[0]]) return
545
+ if (!queryTarget || !queryTarget.elements[ref[0]]) return
388
546
  return ref.some(el => {
389
547
  if (typeof el !== 'string') return
390
548
  const orderbyTarget = queryTarget.elements[el]._isStructured
@@ -398,9 +556,12 @@ const _ifOrderByOrWhereSkip = (queryTarget, ref, model) => {
398
556
  })
399
557
  }
400
558
 
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)
559
+ const _skip = (queryTarget, ref, alias, model) => {
560
+ // we remove the alias, if it exists
561
+ const _ref = ref[0] === alias ? ref.slice(1) : ref
562
+ if (_ref.length === 1) return queryTarget && queryTarget.elements[_ref[0]] && queryTarget.elements[_ref[0]].virtual
563
+
564
+ return _ifOrderByOrWhereSkip(queryTarget, _ref, model)
404
565
  }
405
566
 
406
567
  const _convertOrderByIfSkip = (orderByCQN, index) => {
@@ -412,21 +573,22 @@ const _convertWhereIfSkip = (whereCQN, index) => {
412
573
  OPERATIONS.includes(whereCQN[index + 1]) ? whereCQN.splice(index + 1, 2) : whereCQN.splice(index - 2, 2)
413
574
  }
414
575
 
415
- const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, processFn) => {
576
+ const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, alias, processFn) => {
416
577
  const queryTarget = model.definitions[ensureNoDraftsSuffix(target)]
417
578
 
418
579
  orderByOrWhereCQN.forEach((el, index) => {
419
- if (el.ref && _skip(queryTarget, el.ref, model)) processFn(orderByOrWhereCQN, index)
580
+ if (el.ref && _skip(queryTarget, el.ref, alias, model)) processFn(orderByOrWhereCQN, index)
420
581
  })
421
582
  }
422
583
 
423
584
  const _convertOrderByOrWhereIfSkip = (cqn, target, model) => {
585
+ const alias = cqn.SELECT.from.as
424
586
  if (cqn.SELECT.orderBy && cqn.SELECT.orderBy.length > 1) {
425
- _convertOrderByOrWhereCQN(cqn.SELECT.orderBy, target, model, _convertOrderByIfSkip)
587
+ _convertOrderByOrWhereCQN(cqn.SELECT.orderBy, target, model, alias, _convertOrderByIfSkip)
426
588
  }
427
589
 
428
590
  if (cqn.SELECT.where) {
429
- _convertOrderByOrWhereCQN(cqn.SELECT.where, target, model, _convertWhereIfSkip)
591
+ _convertOrderByOrWhereCQN(cqn.SELECT.where, target, model, alias, _convertWhereIfSkip)
430
592
  }
431
593
  }
432
594
 
@@ -449,7 +611,7 @@ const _convertExpand = expand => {
449
611
  const _convertRefWhereInExpand = columns => {
450
612
  if (columns) {
451
613
  columns.forEach(col => {
452
- if (col.expand) {
614
+ if (col.expand && typeof col.expand !== 'string') {
453
615
  _convertExpand(col.expand)
454
616
  }
455
617
  })
@@ -486,102 +648,115 @@ const _flattenXpr = cqn => {
486
648
  cqn.forEach(_flattenCQN)
487
649
  }
488
650
 
651
+ const _convertPathExpression = (SELECT, model, options = {}) => {
652
+ const prevAlias = SELECT.from.as
653
+ for (const whereEl of SELECT.where || []) {
654
+ if (typeof whereEl === 'object' && whereEl.SELECT) _convertPathExpression(whereEl.SELECT, model)
655
+ }
656
+ const conversion = convertPathExpressionToWhere(SELECT.from, model, options)
657
+ if (!conversion) return
658
+ const { target, alias, where, cardinality, columns, args } = conversion
659
+
660
+ // REVISIT: It's not possible to just change the reference (i.e. SELECT.from.ref = [target])
661
+ // as many parts of our code base still refer to SELECT.from (e.g. authorization)
662
+ if (args) {
663
+ SELECT.from = { ref: [{ id: target, args }] }
664
+ } else {
665
+ SELECT.from = { ref: [target] }
666
+ }
667
+ if (alias) {
668
+ SELECT.from.as = alias
669
+ if (SELECT.where && alias !== prevAlias) {
670
+ SELECT.where = _addAliasToExpression(SELECT.where, alias)
671
+ }
672
+ }
673
+ if (columns) {
674
+ // TODO: use streaming as outer property
675
+ if (options.isStreaming) SELECT.columns = columns
676
+ else {
677
+ if (!SELECT.columns) {
678
+ // Okra always wants to have the key values, remove once we relax this requirement
679
+ if (model.definitions[target] && model.definitions[target].keys) {
680
+ SELECT.columns = Object.keys(model.definitions[target].keys)
681
+ .filter(k => !model.definitions[target].keys[k].isAssociation)
682
+ .map(k => ({ ref: [k] }))
683
+ } else SELECT.columns = []
684
+ }
685
+ SELECT.columns.push(...columns)
686
+ }
687
+ }
688
+ if (cardinality && cardinality.max === 1) {
689
+ SELECT.one = true
690
+ }
691
+ // TODO: REVISIT: We need to add alias to subselect in .where, .columns, .from, ... etc
692
+ if (where) {
693
+ if (options.isDraft) {
694
+ addToWhere({ SELECT }, where)
695
+ } else {
696
+ addToWhere({ SELECT }, removeIsActiveEntityRecursively(where))
697
+ }
698
+ }
699
+ }
700
+
489
701
  // eslint-disable-next-line complexity
490
- const _convertSelect = (query, model, options) => {
491
- const isDB = options.service instanceof cds.DatabaseService
492
- const isDraft = options.draft
702
+ const _convertSelect = (query, model, _options) => {
703
+ const options = Object.assign(
704
+ {
705
+ isDB: _options.service instanceof cds.DatabaseService,
706
+ isDraft: _options.draft,
707
+ isStreaming: query._streaming
708
+ },
709
+ _options
710
+ )
711
+ // ensure query is ql enabled
712
+ if (!(query instanceof Query)) Object.setPrototypeOf(query, Object.getPrototypeOf(SELECT()))
713
+ if (query.SELECT.from && query.SELECT.from.SELECT) {
714
+ if (query.SELECT._4odata) query.SELECT.from.SELECT._4odata = true
715
+ query.SELECT.from = _convertSelect(query.SELECT.from, model, options)
716
+ }
717
+
493
718
  // REVISIT: a temporary workaround for xpr from new parser
494
719
  if (cds.env.features.odata_new_parser) _flattenCQN(query)
495
720
 
496
721
  // lambda functions
497
- _convertLambda(query, model, options)
722
+ convertWhereExists(query, model, options)
498
723
 
499
724
  // 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
725
+ if (query.SELECT._4odata) {
726
+ _convertNotEqual(query.SELECT, 'where')
727
+ _convertNotEqual(query.SELECT, 'having')
528
728
  }
529
729
 
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)
730
+ _convertPathExpression(query.SELECT, model, options)
731
+ rewriteAsterisks(query, model, options.isDB, options.isDraft)
732
+ if (query.SELECT.where && _isCountNavigation(query.SELECT.where)) {
733
+ _convertCountNavigation(query.SELECT, model)
536
734
  }
537
735
 
538
736
  // extract where clause if it is in column expand ref
539
737
  _convertRefWhereInExpand(query.SELECT.columns)
540
738
 
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
- }
739
+ // REVISIT: The following operations only work for _one_ entity.
740
+ // We must also enable them for joins etc.
741
+ const { entityName, alias } = getEntityNameFromCQN(query)
558
742
 
559
- if (cardinality && cardinality.max === 1) {
560
- query.SELECT.one = true
561
- }
743
+ // remove virtual and with skip annotated fields in orderby and where
744
+ _convertOrderByOrWhereIfSkip(query, entityName, model)
562
745
 
563
- if (query.SELECT.search) {
564
- searchOptions = { ...searchOptions, ...{ targetName: target } }
565
- search2cqn4sql(query, model, searchOptions)
746
+ if (query.SELECT.search && !options.suppressSearch) {
747
+ search2cqn4sql(query, model, { ...query._searchOptions, ...{ entityName, alias } })
566
748
  }
567
749
 
568
- if (query.SELECT.where) {
569
- select.where(_addAliasToExpression(query.SELECT.where, select.SELECT.from.as))
750
+ if (query.SELECT.columns && cds.env.effective.odata.structs) {
751
+ flattenStructuredSelect(query, model)
570
752
  }
571
753
 
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)
754
+ // topcount with groupby
755
+ if (query.SELECT.columns && _isBottomTop(query.SELECT.columns)) {
756
+ _createWindowCQN(query.SELECT, model)
582
757
  }
583
758
 
584
- return select
759
+ return query
585
760
  }
586
761
 
587
762
  const _convertInsert = (query, model, options) => {
@@ -732,5 +907,6 @@ const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
732
907
 
733
908
  module.exports = {
734
909
  cqn2cqn4sql,
910
+ convertWhereExists,
735
911
  convertPathExpressionToWhere
736
912
  }