@sap/cds 5.9.2 → 5.9.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 (28) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/compile/for/drafts.js +1 -1
  3. package/lib/index.js +1 -1
  4. package/lib/serve/Service-methods.js +28 -1
  5. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +3 -1
  6. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -3
  7. package/libx/_runtime/common/composition/data.js +22 -13
  8. package/libx/_runtime/common/composition/delete.js +14 -12
  9. package/libx/_runtime/common/generic/input.js +1 -0
  10. package/libx/_runtime/common/generic/put.js +1 -0
  11. package/libx/_runtime/common/utils/cqn.js +5 -10
  12. package/libx/_runtime/common/utils/cqn2cqn4sql.js +35 -66
  13. package/libx/_runtime/common/utils/foreignKeyPropagations.js +28 -8
  14. package/libx/_runtime/common/utils/path.js +3 -3
  15. package/libx/_runtime/common/utils/resolveView.js +3 -0
  16. package/libx/_runtime/common/utils/structured.js +6 -1
  17. package/libx/_runtime/db/Service.js +10 -0
  18. package/libx/_runtime/db/expand/expand-v2.js +13 -5
  19. package/libx/_runtime/db/expand/expandCQNToJoin.js +56 -26
  20. package/libx/_runtime/db/utils/generateAliases.js +9 -0
  21. package/libx/_runtime/extensibility/uiflex/handler/transformWRITE.js +1 -0
  22. package/libx/_runtime/fiori/generic/read.js +83 -31
  23. package/libx/_runtime/fiori/generic/readOverDraft.js +22 -13
  24. package/libx/_runtime/fiori/utils/handler.js +3 -0
  25. package/libx/_runtime/fiori/utils/where.js +38 -25
  26. package/libx/_runtime/hana/search2cqn4sql.js +4 -1
  27. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +19 -12
  28. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## Version 5.9.3 - 2022-04-25
8
+
9
+ ### Fixed
10
+
11
+ - Since 5.8.2 `req.target` for requests like `srv.put('/MyService.entity')` is defined, but `req.query` undefined (before `req.target` was also undefined). This was leading to accessing undefined, which has been fixed.
12
+ - Custom actions with names conflicting with methods from service base classes, e.g. `run()`, could lead to hard-to-detect errors. This is now detected and avoided with a warning.
13
+ - Typed methods for custom actions were erroneously applied to `cds.db` service, which led to server crashes, e.g. when the action was named `deploy()`.
14
+ - Invalid batch requests were further processed after error response was already sent to client, leading to an InternalServerError
15
+ - Full support of `SELECT` queries with operator expressions (`xpr`)
16
+
17
+
7
18
  ## Version 5.9.2 - 2022-04-07
8
19
 
9
20
  ### Fixed
@@ -4,6 +4,6 @@ module.exports = function cds_compile_for_drafts (csn,o) {
4
4
  const unfold = cds_compile_for_drafts.unfold || (
5
5
  cds_compile_for_drafts.unfold = require ('@sap/cds-compiler/lib/transform/draft/odata')
6
6
  )
7
- // csn = JSON.parse (JSON.stringify (csn)) // REVISIT: workaround for bad test setup
7
+ csn = JSON.parse (JSON.stringify (csn)) // REVISIT: workaround for bad test setup
8
8
  return unfold (csn,o||{})
9
9
  }
package/lib/index.js CHANGED
@@ -81,7 +81,7 @@ if (global.cds) Object.assign(module,{exports:global.cds}) ; else {
81
81
  ApplicationService: lazy => require('../libx/_runtime/cds-services/services/Service.js'),
82
82
  MessagingService: lazy => require('../libx/_runtime/messaging/service.js'),
83
83
  DatabaseService: lazy => require('../libx/_runtime/db/Service.js'),
84
- RemoteService: lazy => require('../libx/_runtime/rest/service.js'),
84
+ RemoteService: lazy => require('../libx/_runtime/remote/Service.js'),
85
85
  AuditLogService: lazy => require('../libx/_runtime/audit/Service.js'),
86
86
  odata: require('../libx/odata'),
87
87
 
@@ -1,5 +1,12 @@
1
+ const cds = require('..')
2
+ const LOG = cds.log('cds-app-service-methods')
3
+
1
4
 
2
5
  module.exports = (srv) => {
6
+ if (!( //> we only support that for app services
7
+ srv instanceof cds.ApplicationService ||
8
+ srv instanceof cds.RemoteService
9
+ )) return
3
10
  for (const each of srv.operations) {
4
11
  add_handler_for (srv, each)
5
12
  }
@@ -16,7 +23,23 @@ const add_handler_for = (srv, def) => {
16
23
  // Use existing methods as handler implementations
17
24
  const method = srv[event]
18
25
  if (method) {
19
- if (method._is_stub || method.name in srv.__proto__.__proto__) return
26
+ if (method._is_stub) return
27
+ const baseclass = (
28
+ srv.__proto__ === cds.ApplicationService.prototype ? srv.__proto__ :
29
+ srv.__proto__ === cds.RemoteService.prototype ? srv.__proto__ :
30
+ srv.__proto__.__proto__ // in case of class-based impls
31
+ )
32
+ if (method.name in baseclass) return LOG.warn(`WARNING: custom ${def.kind} '${event}()' conflicts with method in base class.
33
+
34
+ Cannot add typed method for custom ${def.kind} '${event}' to service impl of '${srv.name}',
35
+ as this would shadow equally named method in service base class '${baseclass.constructor.name}'.
36
+ Consider choosing a different name for your custom ${def.kind}.
37
+ Learn more at https://cap.cloud.sap/docs/guides/providing-services#actions-and-functions.
38
+ `)
39
+ LOG.debug (`
40
+ Using method ${event} from service class '${baseclass.constructor.name}'
41
+ as handler for ${def.kind} '${event}' in service '${srv.name}'
42
+ `)
20
43
  srv.on (event, function ({params,data}) {
21
44
  const args = []; if (def.parent) args.push (def.parent)
22
45
  for (let p in params) args.push(params[p])
@@ -26,6 +49,10 @@ const add_handler_for = (srv, def) => {
26
49
  }
27
50
 
28
51
  // Add stub methods to send request via typed API
52
+ LOG.debug (`
53
+ Adding typed method stub for calling custom ${def.kind} '${event}'
54
+ to service impl '${srv.name}'
55
+ `)
29
56
  const stub = srv[event] = function (...args) {
30
57
  const req = { event, data:{} }, $ = args[0]
31
58
  const target = this.entities [ $ && $.name ? $.name.match(/\w*$/)[0] : $ ]
@@ -89,7 +89,9 @@ class BatchRequestListBuilder {
89
89
  source
90
90
  .pipe(reader)
91
91
  .on('finish', () => {
92
- callback(null, this._requestInBatchList)
92
+ //Revisit: if statement needed in node v12 and v14, not in v16.
93
+ //Without it, finish callback reached in 12/14 after error handler was thrown
94
+ if (!source.res || !source.res.headersSent) callback(null, this._requestInBatchList)
93
95
  })
94
96
  .on('error', callback)
95
97
  }
@@ -30,9 +30,7 @@ const _adaptSubSelectsDraft = select => {
30
30
  if (element.SELECT) {
31
31
  _adaptSubSelectsDraft(element)
32
32
  } else if (element.xpr) {
33
- for (const ele of element.xpr.filter(e => e.SELECT)) {
34
- _adaptSubSelectsDraft(ele)
35
- }
33
+ _adaptSubSelectsDraft({ SELECT: { from: {}, where: element.xpr } })
36
34
  }
37
35
  }
38
36
  }
@@ -12,20 +12,13 @@ const { SELECT } = cds.ql
12
12
  * own utils
13
13
  */
14
14
 
15
- const _isSameEntity = (cqn, req) => {
16
- const where = cqn.UPDATE.where || []
17
- const persistentObj = Array.isArray(req._.partialPersistentState)
18
- ? req._.partialPersistentState[0]
19
- : req._.partialPersistentState
20
- if (!persistentObj) {
21
- // If no data was found we don't know if it is the same entity
22
- return false
23
- }
24
- const target = getDBTable(req.target)
25
- if (target.name !== (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) && target.name !== cqn.UPDATE.entity) {
26
- return false
27
- }
15
+ const _isSameEntityInWhere = (where, target, persistentObj) => {
28
16
  for (let i = 0; i < where.length; i++) {
17
+ if (where[i].xpr) {
18
+ const res = _isSameEntityInWhere(where[i].xpr, target, persistentObj)
19
+ if (!res) return res
20
+ continue
21
+ }
29
22
  if (!where[i] || !where[i].ref || !target.elements[where[i].ref]) {
30
23
  continue
31
24
  }
@@ -40,6 +33,22 @@ const _isSameEntity = (cqn, req) => {
40
33
  return true
41
34
  }
42
35
 
36
+ const _isSameEntity = (cqn, req) => {
37
+ const where = cqn.UPDATE.where || []
38
+ const persistentObj = Array.isArray(req._.partialPersistentState)
39
+ ? req._.partialPersistentState[0]
40
+ : req._.partialPersistentState
41
+ if (!persistentObj) {
42
+ // If no data was found we don't know if it is the same entity
43
+ return false
44
+ }
45
+ const target = getDBTable(req.target)
46
+ if (target.name !== (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) && target.name !== cqn.UPDATE.entity) {
47
+ return false
48
+ }
49
+ return _isSameEntityInWhere(where, target, persistentObj)
50
+ }
51
+
43
52
  const _getLinksOfCompTree = compositionTree => {
44
53
  const links = []
45
54
  for (const link of [...compositionTree.backLinks, ...compositionTree.customBackLinks]) {
@@ -216,6 +216,18 @@ const resolveNavigationTarget = (cqn, ref, model) => {
216
216
  return { target, elementName }
217
217
  }
218
218
 
219
+ const _getDataFromOncond = (onCond, parent) => {
220
+ return onCond.reduce((res, e) => {
221
+ if (e.xpr) {
222
+ return Object.assign(res, _getDataFromOncond(e.xpr, parent))
223
+ }
224
+ if (!e.ref || e.ref[0] !== '$$parent') return res
225
+ const fk = e.ref.slice(1).join('_')
226
+ if (!parent.keys[fk]) res[fk] = null
227
+ return res
228
+ }, {})
229
+ }
230
+
219
231
  // eslint-disable-next-line complexity
220
232
  const getSetNullParentForeignKeyCQNs = async (model, req, dbQuery) => {
221
233
  const cqns = []
@@ -229,12 +241,7 @@ const getSetNullParentForeignKeyCQNs = async (model, req, dbQuery) => {
229
241
  const element = parent.elements[elementName]
230
242
  if (element && element._isCompositionEffective && element.is2one) {
231
243
  const onCond = element && parent._relations[element.name].join('$$whatever', '$$parent')
232
- const data = onCond.reduce((res, e) => {
233
- if (!e.ref || e.ref[0] !== '$$parent') return res
234
- const fk = e.ref.slice(1).join('_')
235
- if (!parent.keys[fk]) res[fk] = null
236
- return res
237
- }, {})
244
+ const data = _getDataFromOncond(onCond, parent)
238
245
  cqn.data(data)
239
246
  cqn.__4delete = true
240
247
  if (Object.keys(data).length) cqns.push(cqn)
@@ -247,12 +254,7 @@ const getSetNullParentForeignKeyCQNs = async (model, req, dbQuery) => {
247
254
  for (const element of comp2oneParents) {
248
255
  const parent = element.parent
249
256
  const onCond = parent._relations[element.name].join('$$child', '$$parent')
250
- const data = onCond.reduce((res, e) => {
251
- if (!e.ref || e.ref[0] !== '$$parent') return res
252
- const fk = e.ref.slice(1).join('_')
253
- if (!parent.keys[fk]) res[fk] = null
254
- return res
255
- }, {})
257
+ const data = _getDataFromOncond(onCond, parent)
256
258
  const columns = getColumns(parent, { _4db: true, onlyKeys: true }).map(c => ({ ref: ['$$parent', c.name] }))
257
259
  const as = query.DELETE.from.as || (query.DELETE.from.ref && query.DELETE.from.ref[0]) || query.DELETE.from
258
260
  const selectCQN = SELECT.from(`${parent.name} as $$parent`)
@@ -184,6 +184,7 @@ const _getBoundActionBindingParameter = req => {
184
184
  }
185
185
 
186
186
  function _handler(req) {
187
+ if (!req.query) return // FIXME: the code below expects req.query to be defined
187
188
  if (!req.target) return
188
189
 
189
190
  const template = getTemplate('app-input', this, req.target, { pick: _pick })
@@ -57,6 +57,7 @@ const _pick = element => {
57
57
 
58
58
  function _handler(req) {
59
59
  if (req.method !== 'PUT') return
60
+ if (!req.query) return // FIXME: the code below expects req.query to be defined
60
61
  if (!req.target) return
61
62
 
62
63
  // not for payloads with stream properties
@@ -16,20 +16,16 @@ const getEntityNameFromUpdateCQN = cqn => {
16
16
  return (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) || cqn.UPDATE.entity.name || cqn.UPDATE.entity
17
17
  }
18
18
 
19
- const addToWhere = (cqn, where) => {
20
- const partial = cqn.SELECT || cqn.UPDATE || cqn.DELETE
21
- if (!partial.where) partial.where = where
22
- else {
23
- partial.where.unshift('(')
24
- partial.where.push(')', 'and', '(', ...where, ')')
25
- }
26
- }
27
-
28
19
  // scope: simple wheres à la "[{ ref: ['foo'] }, '=', { val: 'bar' }, 'and', ... ]"
29
20
  function where2obj(where, target = null) {
30
21
  const data = {}
31
22
  for (let i = 0; i < where.length; i++) {
32
23
  const whereEl = where[i]
24
+
25
+ if (whereEl.xpr) {
26
+ where2obj(whereEl.xpr, target, data)
27
+ }
28
+
33
29
  const colName = whereEl.ref && whereEl.ref[whereEl.ref.length - 1]
34
30
  // optional validation if target is passed
35
31
  if (target) {
@@ -85,7 +81,6 @@ function isPathToDraft(path, model) {
85
81
  }
86
82
 
87
83
  module.exports = {
88
- addToWhere,
89
84
  getEntityNameFromDeleteCQN,
90
85
  getEntityNameFromUpdateCQN,
91
86
  where2obj,
@@ -12,7 +12,6 @@ const { getEntityNameFromCQN } = require('./entityFromCqn')
12
12
  const getError = require('../../common/error')
13
13
  const { rewriteAsterisks } = require('./rewriteAsterisks')
14
14
  const { getPathFromRef, getEntityFromPath } = require('../../common/utils/path')
15
- const { addToWhere } = require('../../common/utils/cqn')
16
15
  const { removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
17
16
  const { addRefToWhereIfNecessary } = require('../../../odata/afterburner')
18
17
  const { addAliasToExpression, PARENT_ALIAS, FOREIGN_ALIAS } = require('../../db/utils/generateAliases')
@@ -240,11 +239,14 @@ const _convertCountNavigation = (SELECT, target) => {
240
239
  }
241
240
 
242
241
  const _addTableName = (where, tableName) => {
243
- return where.map(ref => {
244
- if (ref.ref) {
245
- ref.ref.unshift(tableName)
242
+ return where.map(whereEl => {
243
+ if (whereEl.xpr) {
244
+ return { xpr: _addTableName(whereEl.xpr, tableName) }
246
245
  }
247
- return ref
246
+ if (whereEl.ref) {
247
+ whereEl.ref.unshift(tableName)
248
+ }
249
+ return whereEl
248
250
  })
249
251
  }
250
252
 
@@ -444,10 +446,7 @@ const convertWhereExists = (query, model, options, currentTarget) => {
444
446
  outerAlias = as || PARENT_ALIAS + lambdaIteration
445
447
  innerAlias = FOREIGN_ALIAS + lambdaIteration
446
448
  } else {
447
- const element = currentTarget.elements[ref]
448
- if (element && element.isAssociation) {
449
- queryTarget = element._target
450
- }
449
+ queryTarget = getEntityFromPath({ ref }, currentTarget)
451
450
  }
452
451
 
453
452
  if (where) {
@@ -562,6 +561,10 @@ const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, alias, proc
562
561
  const queryTarget = model.definitions[ensureNoDraftsSuffix(target)]
563
562
 
564
563
  orderByOrWhereCQN.forEach((el, index) => {
564
+ if (el.xpr) {
565
+ _convertOrderByOrWhereCQN(el.xpr, target, model, alias, processFn)
566
+ return
567
+ }
565
568
  if (el.ref && _skip(queryTarget, el.ref, alias, model)) processFn(orderByOrWhereCQN, index)
566
569
  })
567
570
  }
@@ -603,86 +606,56 @@ const _convertRefWhereInExpand = columns => {
603
606
  }
604
607
  }
605
608
 
606
- const _flattenCQN = cqn => {
607
- if (Array.isArray(cqn)) cqn.forEach(_flattenCQN)
608
- else if (cqn) {
609
- if (cqn.SELECT) _flattenCQN(cqn.SELECT)
610
- if (cqn.from) _flattenCQN(cqn.from)
611
- if (cqn.ref) _flattenCQN(cqn.ref)
612
- if (cqn.SET) _flattenCQN(cqn.SET)
613
- if (cqn.args) _flattenCQN(cqn.args)
614
- if (cqn.columns) _flattenCQN(cqn.columns)
615
- if (cqn.expand) _flattenCQN(cqn.expand)
616
- if (cqn.where) _flattenXpr(cqn.where)
617
- if (cqn.having) _flattenXpr(cqn.having)
618
- }
619
- }
620
-
621
- const _flattenXpr = cqn => {
622
- if (!Array.isArray(cqn)) {
623
- if (cqn.xpr) cqn = cqn.xpr
624
- return
625
- }
626
-
627
- let idx = cqn.findIndex(el => el.xpr)
628
- while (idx > -1) {
629
- cqn.splice(idx, 1, '(', ...cqn[idx].xpr, ')')
630
- idx = cqn.findIndex(el => el.xpr)
609
+ const _convertPathExpression = (query, model, options = {}) => {
610
+ const prevAlias = query.SELECT.from.as
611
+ for (const whereEl of query.SELECT.where || []) {
612
+ if (typeof whereEl === 'object' && whereEl.SELECT) _convertPathExpression(whereEl, model)
631
613
  }
632
-
633
- cqn.forEach(_flattenCQN)
634
- }
635
-
636
- const _convertPathExpression = (SELECT, model, options = {}) => {
637
- const prevAlias = SELECT.from.as
638
- for (const whereEl of SELECT.where || []) {
639
- if (typeof whereEl === 'object' && whereEl.SELECT) _convertPathExpression(whereEl.SELECT, model)
640
- }
641
- const conversion = convertPathExpressionToWhere(SELECT.from, model, options)
614
+ const conversion = convertPathExpressionToWhere(query.SELECT.from, model, options)
642
615
  if (!conversion) return
643
616
  const { target, alias, where, cardinality, columns, args } = conversion
644
617
 
645
618
  // REVISIT: It's not possible to just change the reference (i.e. SELECT.from.ref = [target])
646
619
  // as many parts of our code base still refer to SELECT.from (e.g. authorization)
647
620
  if (args) {
648
- SELECT.from = { ref: [{ id: target, args }] }
621
+ query.SELECT.from = { ref: [{ id: target, args }] }
649
622
  } else {
650
- SELECT.from = { ref: [target] }
623
+ query.SELECT.from = { ref: [target] }
651
624
  }
652
625
  if (alias) {
653
- SELECT.from.as = alias
654
- if (SELECT.where && alias !== prevAlias) {
655
- SELECT.where = addAliasToExpression(SELECT.where, alias)
626
+ query.SELECT.from.as = alias
627
+ if (query.SELECT.where && alias !== prevAlias) {
628
+ query.SELECT.where = addAliasToExpression(query.SELECT.where, alias)
656
629
  }
657
630
  }
658
631
  if (columns) {
659
632
  // TODO: use streaming as outer property
660
- if (options.isStreaming) SELECT.columns = columns
633
+ if (options.isStreaming) query.SELECT.columns = columns
661
634
  else {
662
- if (!SELECT.columns) {
635
+ if (!query.SELECT.columns) {
663
636
  // Okra always wants to have the key values, remove once we relax this requirement
664
637
  if (model.definitions[target] && model.definitions[target].keys) {
665
- SELECT.columns = Object.keys(model.definitions[target].keys)
638
+ query.SELECT.columns = Object.keys(model.definitions[target].keys)
666
639
  .filter(
667
640
  k =>
668
641
  !model.definitions[target].keys[k].isAssociation &&
669
642
  !columns.find(element => element.ref && element.ref[element.ref.length - 1] === k)
670
643
  )
671
644
  .map(k => ({ ref: [k] }))
672
- } else SELECT.columns = []
645
+ } else query.SELECT.columns = []
673
646
  }
674
- SELECT.columns.push(...columns)
647
+ query.SELECT.columns.push(...columns)
675
648
  }
676
649
  }
677
650
  if (cardinality && cardinality.max === 1) {
678
- SELECT.one = true
651
+ query.SELECT.one = true
679
652
  }
680
653
  // TODO: REVISIT: We need to add alias to subselect in .where, .columns, .from, ... etc
681
654
  if (where) {
682
655
  if (options._4fiori) {
683
- addToWhere({ SELECT }, where)
656
+ query.where(where)
684
657
  } else {
685
- addToWhere({ SELECT }, removeIsActiveEntityRecursively(where))
658
+ query.where(removeIsActiveEntityRecursively(where))
686
659
  }
687
660
  }
688
661
  }
@@ -693,6 +666,9 @@ const _convertToOneEqNullInFilter = (query, target) => {
693
666
 
694
667
  for (let i = 0; i < query.where.length; i++) {
695
668
  const w = query.where[i]
669
+ if (w.xpr) {
670
+ _convertToOneEqNullInFilter({ where: w.xpr }, target)
671
+ }
696
672
  const w2 = query.where[i + 2]
697
673
  if (!w2 || !w.ref || w2.val !== null) {
698
674
  continue
@@ -716,10 +692,6 @@ const _convertToOneEqNullInFilter = (query, target) => {
716
692
 
717
693
  // eslint-disable-next-line complexity
718
694
  const _convertSelect = (query, model, _options) => {
719
- const { _initial } = _options
720
- if (_initial) {
721
- delete _options._initial
722
- }
723
695
  const options = Object.assign(
724
696
  {
725
697
  _4db: _options.service instanceof cds.DatabaseService,
@@ -744,7 +716,7 @@ const _convertSelect = (query, model, _options) => {
744
716
  _convertNotEqual(query.SELECT, 'having')
745
717
  }
746
718
 
747
- _convertPathExpression(query.SELECT, model, options)
719
+ _convertPathExpression(query, model, options)
748
720
  rewriteAsterisks(query, model, options)
749
721
  if (query.SELECT.where) {
750
722
  const entityName =
@@ -795,9 +767,6 @@ const _convertSelect = (query, model, _options) => {
795
767
  }
796
768
  }
797
769
 
798
- // temporary workaround for xpr - cds v5.9.2 only
799
- if (_initial) _flattenCQN(query)
800
-
801
770
  return query
802
771
  }
803
772
 
@@ -937,7 +906,7 @@ const _convertUpdate = (query, model, options) => {
937
906
  */
938
907
  const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
939
908
  if (query.SELECT) {
940
- return _convertSelect(query, model, Object.assign(options, { _initial: true }))
909
+ return _convertSelect(query, model, options)
941
910
  }
942
911
 
943
912
  if (query.UPDATE) {
@@ -1,24 +1,44 @@
1
- const _getSubOns = element => {
2
- // this only works for on conds with `and`, once we support `or` this needs to be adjusted
3
- const newOn = element.on ? element.on.filter(e => e !== '(' && e !== ')') : []
4
- const subOns = []
1
+ const _normalizedRef = o => (o && o.ref && o.ref.length > 1 && o.ref[0] === '$self' ? { ref: o.ref.slice(1) } : o)
2
+
3
+ const _sub = (newOn, subOns = []) => {
5
4
  let currArr = []
6
5
 
7
- for (const onEl of newOn) {
8
- if (currArr.length === 0) subOns.push(currArr)
6
+ for (let i = 0; i < newOn.length; i++) {
7
+ const onEl = newOn[i]
8
+
9
+ if (onEl === 'or') {
10
+ // abort condition for or
11
+ subOns.push([])
12
+ return subOns
13
+ }
14
+ if (onEl.xpr) {
15
+ _sub(onEl.xpr, subOns)
16
+ // after xpr there usually should be and/or
17
+ i++
18
+ continue
19
+ }
20
+ if (currArr.length === 0) {
21
+ subOns.push(currArr)
22
+ }
9
23
  if (onEl !== 'and') currArr.push(onEl)
10
24
  else {
11
25
  currArr = []
12
26
  }
13
27
  }
14
28
 
29
+ return subOns
30
+ }
31
+
32
+ const _getSubOns = element => {
33
+ const newOn = element.on || []
34
+ const subOns = _sub(newOn)
35
+
15
36
  for (const subOn of subOns) {
16
37
  // We don't support anything else than
17
38
  // A = B AND C = D AND ...
18
39
  if (subOn.length !== 3) return []
19
40
  }
20
-
21
- return subOns
41
+ return subOns.map(subOn => subOn.map(ref => _normalizedRef(ref)))
22
42
  }
23
43
 
24
44
  const _parentFieldsFromSimpleOnCond = (element, subOn) => {
@@ -17,14 +17,14 @@ const getPathFromRef = ref => {
17
17
  /*
18
18
  * returns the target entity for the given path
19
19
  */
20
- const getEntityFromPath = (path, model) => {
21
- let current = { elements: model.definitions }
20
+ const getEntityFromPath = (path, def) => {
21
+ let current = def.definitions ? { elements: def.definitions } : def
22
22
  path = typeof path === 'string' ? cds.parse.path(path) : path
23
23
  const segments = [...path.ref]
24
24
  while (segments.length) {
25
25
  const segment = segments.shift()
26
26
  current = current.elements[segment.id || segment]
27
- if (current && current.target) current = model.definitions[current.target]
27
+ if (current && current.target) current = current._target
28
28
  }
29
29
  return current
30
30
  }
@@ -243,6 +243,9 @@ const _newEntries = (entries = [], transition, service) =>
243
243
 
244
244
  const _newWhere = (where = [], transition, tableName, alias, isSubselect = false) => {
245
245
  const newWhere = where.map(whereElement => {
246
+ if (whereElement.xpr) {
247
+ return { xpr: _newWhere(whereElement.xpr, transition, tableName, alias, isSubselect) }
248
+ }
246
249
  const newWhereElement = { ...whereElement }
247
250
  if (!whereElement.ref && !whereElement.SELECT && !whereElement.func) return whereElement
248
251
  if (whereElement.SELECT && whereElement.SELECT.where) {
@@ -47,7 +47,8 @@ const _flattenStructuredInExpand = (column, { _target: expandedEntity }) => {
47
47
  if (orderBy) {
48
48
  column.orderBy = orderBy
49
49
  }
50
- column.where = flattenStructuredWhereHaving(column.where, expandedEntity)
50
+ const columnWhere = flattenStructuredWhereHaving(column.where, expandedEntity)
51
+ if (columnWhere) column.where = columnWhere
51
52
  column.expand = column.expand.filter(e => !e.ref || !toBeDeleted.includes(e.ref[e.ref.length - 1]))
52
53
  column.expand.push(...flattenedElements)
53
54
  }
@@ -205,6 +206,10 @@ const flattenStructuredWhereHaving = (filterArray, csnEntity, model) => {
205
206
 
206
207
  const newFilterArray = []
207
208
  for (let i = 0; i < filterArray.length; i++) {
209
+ if (filterArray[i].xpr) {
210
+ newFilterArray.push({ xpr: flattenStructuredWhereHaving(filterArray[i].xpr, csnEntity, model) })
211
+ continue
212
+ }
208
213
  if (filterArray[i + 1] in OPERATIONS_MAP) {
209
214
  const refElement = filterArray[i].ref ? filterArray[i] : filterArray[i + 2]
210
215
 
@@ -38,6 +38,16 @@ class DatabaseService extends cds.Service {
38
38
  // REVISIT: how to generic handler registration?
39
39
  }
40
40
 
41
+ /** Database services don't support custom-defined operations */
42
+ operations() {
43
+ return []
44
+ }
45
+
46
+ /** Database services don't support custom-defined events */
47
+ events() {
48
+ return []
49
+ }
50
+
41
51
  /*
42
52
  * tx
43
53
  */
@@ -79,16 +79,24 @@ const _autoExpandNavsAndAttachToResult = async (entity, previousResult, depth, o
79
79
  return previousResult
80
80
  }
81
81
 
82
+ const _fkForOnCOnd = (onCond, requiredFks) => {
83
+ for (const ele of onCond) {
84
+ if (ele.xpr) {
85
+ _fkForOnCOnd(ele.xpr, requiredFks)
86
+ }
87
+
88
+ if (ele.ref && ele.ref[0] === 'parent') {
89
+ requiredFks.add(ele.ref.slice(1).join('_'))
90
+ }
91
+ }
92
+ }
93
+
82
94
  const _foreignKeysOfTopLevelNavs = (entity, options) => {
83
95
  const requiredFks = new Set()
84
96
  for (const nav in entity._associations) {
85
97
  if (options.onlyCompositions && entity._associations[nav]._isAssociationEffective) continue
86
98
  const onCond = entity._relations[nav].join('child', 'parent')
87
- for (const ele of onCond) {
88
- if (ele.ref && ele.ref[0] === 'parent') {
89
- requiredFks.add(ele.ref.slice(1).join('_'))
90
- }
91
- }
99
+ _fkForOnCOnd(onCond, requiredFks)
92
100
  }
93
101
  return [...requiredFks]
94
102
  }
@@ -395,6 +395,9 @@ class JoinCQNFromExpanded {
395
395
  */
396
396
  _adaptWhereSELECT(aliasedTable, where, tableAlias) {
397
397
  return where.map(element => {
398
+ if (element.xpr) {
399
+ return { xpr: this._adaptWhereSELECT(aliasedTable, element.xpr, tableAlias) }
400
+ }
398
401
  return this._elementAliasNeedsReplacement(element, aliasedTable)
399
402
  ? Object.assign({}, element, { ref: [tableAlias, element.ref[1]] })
400
403
  : element
@@ -870,13 +873,21 @@ class JoinCQNFromExpanded {
870
873
  const aliases = this._getAliases(subSelectColumns)
871
874
  const on = entity._relations[tableAlias === columns[0] ? columns.slice(1) : columns].join(tableAlias, parentAlias)
872
875
 
876
+ this._adjustAliases(on, aliases)
877
+
878
+ return on
879
+ }
880
+
881
+ _adjustAliases(on, aliases) {
873
882
  for (const element of on) {
883
+ if (element.xpr) {
884
+ this._adjustAliases(element.xpr, aliases)
885
+ continue
886
+ }
874
887
  if (element.ref && aliases[element.ref[0]] && aliases[element.ref[0]][element.ref[1]]) {
875
888
  element.ref[1] = aliases[element.ref[0]][element.ref[1]]
876
889
  }
877
890
  }
878
-
879
- return on
880
891
  }
881
892
 
882
893
  _addJoinKeyColumnsToUnion(args, on, parentAlias) {
@@ -899,23 +910,27 @@ class JoinCQNFromExpanded {
899
910
  }
900
911
 
901
912
  _addColumns(columns, on, parentAlias, withAlias = false) {
902
- const keyColumns = on
903
- .filter(entry => {
904
- return (
905
- entry.ref &&
906
- entry.ref[0] === parentAlias &&
907
- !columns.some(column => column.ref && column.ref[column.ref.length - 1] === entry.ref[1])
913
+ for (const entry of on) {
914
+ if (entry.xpr) {
915
+ this._addColumns(columns, entry.xpr, parentAlias, withAlias)
916
+ continue
917
+ }
918
+ if (
919
+ entry.ref &&
920
+ entry.ref[0] === parentAlias &&
921
+ !columns.some(column => column.ref && column.ref[column.ref.length - 1] === entry.ref[1])
922
+ ) {
923
+ columns.push(
924
+ withAlias
925
+ ? { ref: [parentAlias, entry.ref[1]], as: `${parentAlias}_${entry.ref[1]}` }
926
+ : { ref: [entry.ref[1]] }
908
927
  )
909
- })
910
- .map(entry =>
911
- withAlias ? { ref: [parentAlias, entry.ref[1]], as: `${parentAlias}_${entry.ref[1]}` } : { ref: [entry.ref[1]] }
912
- )
913
- if (keyColumns.length === 0) return
914
- columns.push(...keyColumns)
928
+ }
929
+ }
915
930
  }
916
931
 
917
932
  /**
918
- * Add an unique alias to each column, to avoid ambiguity.
933
+ * Add a unique alias to each column, to avoid ambiguity.
919
934
  * Add this information to the post process config.
920
935
  *
921
936
  * @param {object} column
@@ -1318,6 +1333,26 @@ class JoinCQNFromExpanded {
1318
1333
  return newObj
1319
1334
  }
1320
1335
 
1336
+ _getFilterColumns(readToOneCQN, on, parentAlias) {
1337
+ const columns = []
1338
+ const outerColumns = []
1339
+
1340
+ for (const entry of on) {
1341
+ if (entry.xpr) {
1342
+ const { columns: cols, outerColumns: outerCols } = this._getFilterColumns(readToOneCQN, entry.xpr, parentAlias)
1343
+ columns.push(...cols)
1344
+ outerColumns.push(...outerCols)
1345
+ continue
1346
+ }
1347
+ if (typeof entry === 'object' && entry.ref && entry.ref[0] === 'filterExpand') {
1348
+ columns.push(this._getColumnObjectForFilterExpand(readToOneCQN, parentAlias, entry.ref[1]))
1349
+ outerColumns.push({ ref: [entry.ref[1]] })
1350
+ }
1351
+ }
1352
+
1353
+ return { columns, outerColumns }
1354
+ }
1355
+
1321
1356
  /**
1322
1357
  * Reduce column list to column(s) needed to merge the result into one.
1323
1358
  *
@@ -1329,22 +1364,13 @@ class JoinCQNFromExpanded {
1329
1364
  * @private
1330
1365
  */
1331
1366
  _getFilterExpandCQN(readToOneCQN, on, parentAlias, keyObject) {
1332
- const columns = []
1333
-
1334
- const outerColumns = []
1335
-
1336
- for (const entry of on) {
1337
- if (typeof entry === 'object' && entry.ref && entry.ref[0] === 'filterExpand') {
1338
- columns.push(this._getColumnObjectForFilterExpand(readToOneCQN, parentAlias, entry.ref[1]))
1339
- outerColumns.push({ ref: [entry.ref[1]] })
1340
- }
1341
- }
1342
-
1343
1367
  const keys = Object.keys(keyObject).filter(
1344
1368
  key =>
1345
1369
  key !== 'IsActiveEntity' && !keyObject[key].is2one && !keyObject[key].is2many && !keyObject[key]._isStructured
1346
1370
  )
1347
1371
 
1372
+ const { columns, outerColumns } = this._getFilterColumns(readToOneCQN, on, parentAlias)
1373
+
1348
1374
  for (const key of keys) {
1349
1375
  if (!columns.map(entry => entry.as).includes(key)) {
1350
1376
  columns.push(this._getColumnObjectForFilterExpand(readToOneCQN, parentAlias, key))
@@ -1455,6 +1481,10 @@ class JoinCQNFromExpanded {
1455
1481
  this._addColumNames(entity, parentAlias, columnNames)
1456
1482
 
1457
1483
  for (const entry of on) {
1484
+ if (entry.xpr) {
1485
+ columns.push(...this._getJoinColumnsFromOnAddToMapping(mapping, parentAlias, entry.xpr, entity))
1486
+ continue
1487
+ }
1458
1488
  if (typeof entry === 'object' && entry.ref && entry.ref[0] !== 'filterExpand') {
1459
1489
  const as = entry.ref.join('_')
1460
1490
  columns.push({
@@ -20,6 +20,11 @@ const _redirectXpr = (xpr, aliasMap) => {
20
20
  return
21
21
  }
22
22
 
23
+ if (element.xpr) {
24
+ _redirectXpr(element.xpr, aliasMap)
25
+ return
26
+ }
27
+
23
28
  if (element.func) {
24
29
  _redirectXpr(element.args, aliasMap)
25
30
  return
@@ -98,6 +103,10 @@ const generateAliases = query => {
98
103
 
99
104
  const _addParentAlias = (where, alias) => {
100
105
  where.forEach(e => {
106
+ if (e.xpr) {
107
+ _addParentAlias(e.xpr, alias)
108
+ return
109
+ }
101
110
  if (e.ref && e.ref[0].match(PARENT_ALIAS_REGEX)) {
102
111
  e.ref[0] = alias
103
112
  }
@@ -21,6 +21,7 @@ const _processorFn = ({ row, key }) => {
21
21
  }
22
22
 
23
23
  function transformExtendedFieldsCREATE(req) {
24
+ if (!req.query) return // FIXME: the code below expects req.query to be defined
24
25
  if (!req.target) return
25
26
  if (!req.query.INSERT.entries) return // REVISIT: breaks at cds.deploy -> should anyways not kick in during cds.deploy
26
27
 
@@ -19,9 +19,18 @@ const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively
19
19
  const { getColumns } = require('../../cds-services/services/utils/columns')
20
20
  const { adaptStreamCQN } = require('../../cds-services/adapter/odata-v4/utils/stream')
21
21
 
22
+ const _findSubselect = where => {
23
+ return where.find((e, i) => {
24
+ if (e.xpr) {
25
+ return _findSubselect(e.xpr)
26
+ }
27
+ return e.SELECT && where[i - 1] === 'exists'
28
+ })
29
+ }
30
+
22
31
  const _findRootSubSelectFor = query => {
23
32
  if (query.SELECT.where) {
24
- const subSelect = query.SELECT.where.find((e, i) => e.SELECT && query.SELECT.where[i - 1] === 'exists')
33
+ const subSelect = _findSubselect(query.SELECT.where)
25
34
  return subSelect ? _findRootSubSelectFor(subSelect) : query
26
35
  }
27
36
  return query
@@ -422,8 +431,8 @@ const _allActive = (req, columns, model) => {
422
431
  cqn.leftJoin(ensureDraftsSuffix(table.ref[0]) + ' as drafts').on(`${table.as}.${ids[0]} = drafts.${ids[0]}`)
423
432
 
424
433
  for (let i = 1; i < ids.length; i++) {
425
- // REVISIT: this is extremely expensive as it repeatedly invokes the compiler's cds.parse.expr -> better extend plain CQN yourself here
426
- cqn.and(`${table.as}.${ids[i]} = drafts.${ids[i]}`)
434
+ // this 'and' belongs to the join condition and is not a where and
435
+ cqn.and({ ref: [table.as, ids[i]] }, '=', { ref: ['drafts', ids[i]] })
427
436
  }
428
437
  }
429
438
 
@@ -557,27 +566,38 @@ const _alignAliasForUnion = (table, as, select) => {
557
566
  return select
558
567
  }
559
568
 
560
- const _findJoinInQuery = (query, parentAlias) => {
561
- const targetAlias = query.SELECT.from.as
562
- const isTargetRef = el => targetAlias && el.ref && el.ref.length > 1 && el.ref[0] === targetAlias
563
- if (query.SELECT && query.SELECT.where)
564
- return query.SELECT.where.reduce((links, el, idx, where) => {
565
- if (el.ref && el.ref[0] === parentAlias && el.ref[el.ref.length - 1] !== 'IsActiveEntity') {
566
- if (where[idx - 1] && where[idx - 1] === '=' && isTargetRef(where[idx - 2])) {
567
- if (links.length) links.push('and')
568
- links.push(el, '=', where[idx - 2])
569
- }
570
- if (where[idx + 1] && where[idx + 1] === '=' && isTargetRef(where[idx + 2])) {
571
- if (links.length) links.push('and')
572
- links.push(el, '=', where[idx + 2])
573
- }
569
+ const isTargetRef = (el, targetAlias) => targetAlias && el.ref && el.ref.length > 1 && el.ref[0] === targetAlias
570
+
571
+ const _joinFromWhere = (where, parentAlias, targetAlias) => {
572
+ return where.reduce((links, el, idx, where) => {
573
+ if (el.xpr) {
574
+ const result = _joinFromWhere(el.xpr, parentAlias, targetAlias)
575
+ if (result.length) {
576
+ if (links.length) links.push('and')
577
+ links.push(...result)
574
578
  }
575
579
  return links
576
- }, [])
577
- return []
580
+ }
581
+
582
+ if (el.ref && el.ref[0] === parentAlias && el.ref[el.ref.length - 1] !== 'IsActiveEntity') {
583
+ if (where[idx - 1] && where[idx - 1] === '=' && isTargetRef(where[idx - 2], targetAlias)) {
584
+ if (links.length) links.push('and')
585
+ links.push(el, '=', where[idx - 2])
586
+ }
587
+ if (where[idx + 1] && where[idx + 1] === '=' && isTargetRef(where[idx + 2], targetAlias)) {
588
+ if (links.length) links.push('and')
589
+ links.push(el, '=', where[idx + 2])
590
+ }
591
+ }
592
+ return links
593
+ }, [])
578
594
  }
579
595
 
580
- const _isFiltered = where => where.some(element => !(element in ['(', ')']))
596
+ const _findJoinInQuery = (query, parentAlias) => {
597
+ const targetAlias = query.SELECT.from.as
598
+ if (query.SELECT && query.SELECT.where) return _joinFromWhere(query.SELECT.where, parentAlias, targetAlias)
599
+ return []
600
+ }
581
601
 
582
602
  const _isDraftField = element => element.ref && element.ref.length > 1 && element.ref[0] === 'DraftAdministrativeData'
583
603
 
@@ -597,6 +617,10 @@ const _isLogicalFunction = (where, index) => {
597
617
  const _getWhereForActive = where => {
598
618
  const activeWhere = []
599
619
  for (let i = 0; i < where.length; i++) {
620
+ if (where[i].xpr) {
621
+ activeWhere.push({ xpr: _getWhereForActive(where[i].xpr) })
622
+ continue
623
+ }
600
624
  if (_isDraftField(where[i])) {
601
625
  activeWhere.push({ val: null })
602
626
  } else if (_functionContainsDraftField(where[i])) {
@@ -721,10 +745,20 @@ const _getSiblingScenario = (req, columns, model, siblingIndex, nav) => {
721
745
  const draftAdminAlias = _isDraftAdminScenario(req) && req.query.SELECT.from.as
722
746
  const params = [...req.params].reverse()
723
747
  const _getSiblingQueryFromWhere = (query, queryIndex, parentQuery) => {
724
- if (query.SELECT && query.SELECT.where) {
725
- const indexExists = query.SELECT.where.indexOf('exists')
726
- if (indexExists > -1 && queryIndex > 0) {
727
- return _getSiblingQueryFromWhere(query.SELECT.where[indexExists + 1], queryIndex - 1, query)
748
+ if (query.SELECT && query.SELECT.where && queryIndex > 0) {
749
+ for (let i = 0; i < query.SELECT.where.length; i++) {
750
+ if (query.SELECT.where[i].xpr && queryIndex > 0) {
751
+ const sibilingQueryFromWhere = _getSiblingQueryFromWhere(
752
+ { SELECT: { where: query.SELECT.where[i].xpr } },
753
+ queryIndex - 1,
754
+ query
755
+ )
756
+ if (sibilingQueryFromWhere) return sibilingQueryFromWhere
757
+ }
758
+
759
+ if (query.SELECT.where[i] === 'exists' && queryIndex > 0) {
760
+ return _getSiblingQueryFromWhere(query.SELECT.where[i + 1], queryIndex - 1, query)
761
+ }
728
762
  }
729
763
  }
730
764
  const target = { name: query.SELECT.from.ref[0].id || query.SELECT.from.ref[0], as: query.SELECT.from.as }
@@ -741,9 +775,16 @@ const _getSiblingScenario = (req, columns, model, siblingIndex, nav) => {
741
775
  return _getSiblingQueryFromWhere(req.query, siblingIndex)
742
776
  }
743
777
 
744
- const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
745
- const _replaceWhereExists = (query, _siblingIndex) => {
746
- if (query.SELECT && query.SELECT.where) {
778
+ const _replaceWhereExists = (query, _siblingIndex, siblingCQN) => {
779
+ if (query.SELECT && query.SELECT.where) {
780
+ for (let i = 0; i < query.SELECT.where.length; i++) {
781
+ const whereElement = query.SELECT.where[i]
782
+ if (whereElement.xpr) {
783
+ const res = _replaceWhereExists({ SELECT: { where: whereElement.xpr } }, _siblingIndex, siblingCQN)
784
+ if (res) return res
785
+ continue
786
+ }
787
+
747
788
  const indexExists = query.SELECT.where.indexOf('exists')
748
789
  if (indexExists > -1) {
749
790
  if (_siblingIndex > 0) return _replaceWhereExists(query.SELECT.where[indexExists + 1], _siblingIndex - 1)
@@ -751,7 +792,10 @@ const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
751
792
  }
752
793
  }
753
794
  }
754
- return _replaceWhereExists(cqn, siblingIndex)
795
+ }
796
+
797
+ const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
798
+ return _replaceWhereExists(cqn, siblingIndex, siblingCQN)
755
799
  }
756
800
 
757
801
  const _getDraftDoc = (req, draftName, draftWhere) => {
@@ -796,6 +840,11 @@ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
796
840
 
797
841
  const _replaceDraftAlias = where => {
798
842
  where.forEach(element => {
843
+ if (element.xpr) {
844
+ _replaceDraftAlias(element.xpr)
845
+ return
846
+ }
847
+
799
848
  if (_isDraftField(element)) {
800
849
  element.ref[0] = 'filterAdmin'
801
850
  }
@@ -981,7 +1030,10 @@ const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, column
981
1030
  _isValidDraftOfWhichIAmOwner(draftParameters.isActiveEntity) && _draftOfWhichIAmOwner(req, draftWhere, columns)
982
1031
 
983
1032
  const _draftInSubSelect = (where, req) => {
984
- return where.some(({ SELECT }) => {
1033
+ return where.some(({ SELECT, xpr }) => {
1034
+ if (xpr) {
1035
+ return _draftInSubSelect(xpr, req)
1036
+ }
985
1037
  if (SELECT && SELECT.where) {
986
1038
  const isActiveEntity = readAndDeleteKeywords(['IsActiveEntity'], SELECT.where, false)
987
1039
  if (isActiveEntity) {
@@ -1022,7 +1074,7 @@ const _generateCQN = (originalFrom, req, columns, model) => {
1022
1074
  return _draftAdminTable(req)
1023
1075
  }
1024
1076
 
1025
- if (!req.query.SELECT.where || !_isFiltered(req.query.SELECT.where)) {
1077
+ if (!req.query.SELECT.where) {
1026
1078
  return _allActive(req, columns, model)
1027
1079
  }
1028
1080
 
@@ -1032,7 +1084,7 @@ const _generateCQN = (originalFrom, req, columns, model) => {
1032
1084
  if (
1033
1085
  draftParameters.isActiveEntity &&
1034
1086
  _isTrue(draftParameters.isActiveEntity.value.val) &&
1035
- !draftParameters.siblingIsActive &&
1087
+ !(draftParameters.siblingIsActive && draftParameters.siblingIsActive.value.val === null) &&
1036
1088
  !draftParameters.hasDraftEntity
1037
1089
  ) {
1038
1090
  return _allActive(req, columns, model)
@@ -5,18 +5,14 @@ const { readAndDeleteKeywords } = require('../utils/where')
5
5
  const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
6
6
  const { isActiveEntityRequested } = require('../../../_runtime/fiori/utils/where')
7
7
 
8
- const _modifyCQN = (cqnDraft, where, context) => {
9
- const whereDraft = [...where]
10
- const result = readAndDeleteKeywords(['IsActiveEntity'], whereDraft)
11
- cqnDraft.where(whereDraft)
8
+ const _modifyWhere = (where, context) => {
9
+ for (let i = 0; i < where.length; i++) {
10
+ const element = where[i]
12
11
 
13
- if (result && result.value.val === false) {
14
- const fromRef = cqnDraft.SELECT.from.ref
15
- cqnDraft.SELECT.from.ref[fromRef.length - 1] = ensureDraftsSuffix(fromRef[fromRef.length - 1])
16
- }
17
-
18
- for (let i = 0; i < cqnDraft.SELECT.where.length; i++) {
19
- const element = cqnDraft.SELECT.where[i]
12
+ if (element.xpr) {
13
+ _modifyWhere(element.xpr, context)
14
+ continue
15
+ }
20
16
 
21
17
  if (element.SELECT) {
22
18
  const subCqnDraft = SELECT.from(
@@ -27,12 +23,25 @@ const _modifyCQN = (cqnDraft, where, context) => {
27
23
  [1]
28
24
  )
29
25
 
30
- cqnDraft.SELECT.where[i] = subCqnDraft
26
+ where[i] = subCqnDraft
31
27
  _modifyCQN(subCqnDraft, element.SELECT.where, context)
32
28
  }
33
29
  }
34
30
  }
35
31
 
32
+ const _modifyCQN = (cqnDraft, where, context) => {
33
+ const whereDraft = [...where]
34
+ const result = readAndDeleteKeywords(['IsActiveEntity'], whereDraft)
35
+ cqnDraft.where(whereDraft)
36
+
37
+ if (result && result.value.val === false) {
38
+ const fromRef = cqnDraft.SELECT.from.ref
39
+ cqnDraft.SELECT.from.ref[fromRef.length - 1] = ensureDraftsSuffix(fromRef[fromRef.length - 1])
40
+ }
41
+
42
+ _modifyWhere(cqnDraft.SELECT.where, context)
43
+ }
44
+
36
45
  const _hasNavToNonDraftEnclosedAssoc = (pathSegments, definitions, excludeAssoc) => {
37
46
  if (pathSegments.length < 2) return false
38
47
  const entity = definitions[pathSegments[0]]
@@ -112,7 +121,7 @@ const _readOverDraftHandler = async function (req, next) {
112
121
 
113
122
  const hasDraftEntity = hasDraft(definitions, sqlQuery)
114
123
 
115
- if (hasDraftEntity && sqlQuery.SELECT.where && sqlQuery.SELECT.where.length > 0) {
124
+ if (hasDraftEntity && sqlQuery.SELECT.where && sqlQuery.SELECT.where.length) {
116
125
  let cqnDraft = SELECT.from({
117
126
  ref: [...sqlQuery.SELECT.from.ref],
118
127
  as: sqlQuery.SELECT.from.as
@@ -93,6 +93,9 @@ const getUpdateDraftAdminCQN = ({ user }, draftUUID) => {
93
93
  const _addAlias = (where, tableName) => {
94
94
  // copy where
95
95
  return where.map(element => {
96
+ if (element.xpr) {
97
+ return { xpr: _addAlias(element.xpr, tableName) }
98
+ }
96
99
  if (element.ref && element.ref.length === 1) {
97
100
  // and copy ref
98
101
  return { ref: [tableName, element.ref[0]] }
@@ -57,7 +57,9 @@ const _removeIsActiveEntityCondition = where => {
57
57
  } else if (where[i] === 'and' && where[i + 1] === '(' && _isActiveEntity(where[i + 2])) {
58
58
  i = i + 6
59
59
  } else if (where[i].xpr) {
60
- newWhere.push({ xpr: _removeIsActiveEntityCondition(where[i].xpr) })
60
+ if (where[i].xpr.length) {
61
+ newWhere.push({ xpr: _removeIsActiveEntityCondition(where[i].xpr) })
62
+ }
61
63
  i++
62
64
  } else {
63
65
  newWhere.push(where[i])
@@ -65,7 +67,7 @@ const _removeIsActiveEntityCondition = where => {
65
67
  }
66
68
  }
67
69
 
68
- if (newWhere[0] === 'and') {
70
+ if (newWhere[0] === 'and' || newWhere[0] === 'or') {
69
71
  newWhere.splice(0, 1)
70
72
  } else if (newWhere[0] === '(' && newWhere[1] === 'and') {
71
73
  newWhere.splice(0, 2)
@@ -90,32 +92,35 @@ const deleteCondition = (index, whereCondition, isXpr = false) => {
90
92
  }
91
93
 
92
94
  const readAndDeleteKeywords = (keywords, whereCondition, toDelete = true) => {
93
- let index = whereCondition.findIndex(({ xpr }) => xpr)
94
- if (index !== -1) {
95
- const result = readAndDeleteKeywords(keywords, whereCondition[index].xpr, toDelete)
96
- if (result) {
97
- if (whereCondition[index].xpr.length === 0) {
98
- deleteCondition(index, whereCondition, true)
95
+ let index = -1
96
+ for (let i = 0; i < whereCondition.length; i++) {
97
+ const entry = whereCondition[i]
98
+ if (entry.xpr) {
99
+ const result = readAndDeleteKeywords(keywords, entry.xpr, toDelete)
100
+ if (result) {
101
+ if (entry.xpr.length === 0) {
102
+ deleteCondition(i, whereCondition, true)
103
+ }
104
+ return result
105
+ }
106
+ } else if (entry.ref) {
107
+ const refLastIndex = entry.ref.length - 1
108
+
109
+ if (keywords.length === 1) {
110
+ if (entry.ref[refLastIndex] === keywords[0]) {
111
+ index = i
112
+ break
113
+ }
99
114
  }
100
- return result
101
- }
102
- }
103
-
104
- index = whereCondition.findIndex(({ ref }) => {
105
- if (!ref) {
106
- return false
107
- }
108
-
109
- const refLastIndex = ref.length - 1
110
-
111
- if (keywords.length === 1) {
112
- return ref[refLastIndex] === keywords[0]
113
- }
114
115
 
115
- if (keywords.length === 2 && ref.length >= 2) {
116
- return ref[refLastIndex - 1] === keywords[0] && ref[refLastIndex] === keywords[1]
116
+ if (keywords.length === 2 && entry.ref.length >= 2) {
117
+ if (entry.ref[refLastIndex - 1] === keywords[0] && entry.ref[refLastIndex] === keywords[1]) {
118
+ index = i
119
+ break
120
+ }
121
+ }
117
122
  }
118
- })
123
+ }
119
124
 
120
125
  if (index === -1) {
121
126
  return
@@ -135,6 +140,10 @@ const readAndDeleteKeywords = (keywords, whereCondition, toDelete = true) => {
135
140
 
136
141
  const removeIsActiveEntityRecursively = where => {
137
142
  for (const entry of where) {
143
+ if (entry.xpr) {
144
+ entry.xpr = removeIsActiveEntityRecursively(entry.xpr)
145
+ continue
146
+ }
138
147
  if (entry.SELECT && entry.SELECT.where && entry.SELECT.from.ref && !entry.SELECT.from.ref[0].endsWith('_drafts')) {
139
148
  entry.SELECT.where = removeIsActiveEntityRecursively(entry.SELECT.where)
140
149
 
@@ -153,6 +162,10 @@ const isActiveEntityRequested = where => {
153
162
  let i = 0
154
163
 
155
164
  while (where[i]) {
165
+ if (where[i].xpr) {
166
+ const isRequested = isActiveEntityRequested(where.xpr)
167
+ if (isRequested) return true
168
+ }
156
169
  if (
157
170
  where[i].ref &&
158
171
  where[i].ref[where[i].ref.length - 1] === 'IsActiveEntity' &&
@@ -37,8 +37,11 @@ const search2cqn4sql = (query, entity, options) => {
37
37
  if (resolveLocalizedDataAtRuntime) {
38
38
  const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
39
39
 
40
+ // REVISIT this is dirty but works for now
40
41
  // replace $user_locale placeholder with the user locale or the HANA session context
41
- onCondition[onCondition.length - 2] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
42
+ if (onCondition[0].xpr) {
43
+ onCondition[0].xpr[onCondition[0].xpr.length - 1] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
44
+ } else onCondition[onCondition.length - 2] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
42
45
 
43
46
  // inner join the target table with the _texts table (the _texts table contains
44
47
  // the translated texts)
@@ -17,29 +17,36 @@ const _getConvertibleEntries = req => {
17
17
  return [...orders, ...groups, ...filters, ...havings]
18
18
  }
19
19
 
20
- // REVISIT once sql can handle structured keys properly, this handler should not be required anymore
21
- const _handler = function (req) {
22
- // do simple checks upfront and exit early
23
- if (!req.query || typeof req.query === 'string') return
24
- if (!req.query.SELECT.orderBy && !req.query.SELECT.groupBy && !req.query.SELECT.where && !req.query.SELECT.having) {
25
- return
26
- }
27
-
28
- if (!req.target || !req.target.elements) return
20
+ const _convert = (refEntries, req) => {
21
+ for (const refEntry of refEntries) {
22
+ if (refEntry.xpr) {
23
+ _convert(refEntry.xpr, req)
24
+ continue
25
+ }
29
26
 
30
- for (const refEntry of _getConvertibleEntries(req)) {
31
27
  if (!refEntry.ref || refEntry.ref.length < 2) {
32
28
  // only check refs in format {ref: ['assoc', 'id']}
33
29
  continue
34
30
  }
35
-
36
31
  const element = req.target.elements[refEntry.ref[0]]
37
32
  if (!element || !element.is2one) return
38
-
39
33
  _convertRefForAssocToOneManaged(element, refEntry)
40
34
  }
41
35
  }
42
36
 
37
+ // REVISIT once sql can handle structured keys properly, this handler should not be required anymore
38
+ const _handler = function (req) {
39
+ // do simple checks upfront and exit early
40
+ if (!(req.query && req.query.SELECT) || typeof req.query === 'string') return
41
+ if (!req.query.SELECT.orderBy && !req.query.SELECT.groupBy && !req.query.SELECT.where && !req.query.SELECT.having) {
42
+ return
43
+ }
44
+
45
+ if (!req.target || !req.target.elements) return
46
+
47
+ _convert(_getConvertibleEntries(req), req)
48
+ }
49
+
43
50
  _handler._initial = true
44
51
 
45
52
  module.exports = _handler
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "5.9.2",
3
+ "version": "5.9.3",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [