@sap/cds 5.9.2 → 5.9.5

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 (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/lib/compile/for/drafts.js +1 -1
  3. package/lib/index.js +1 -1
  4. package/lib/serve/Service-methods.js +47 -1
  5. package/libx/_runtime/auth/index.js +16 -1
  6. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +3 -1
  7. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -3
  8. package/libx/_runtime/common/aspects/utils.js +8 -2
  9. package/libx/_runtime/common/composition/data.js +22 -13
  10. package/libx/_runtime/common/composition/delete.js +14 -12
  11. package/libx/_runtime/common/generic/auth/expand.js +1 -0
  12. package/libx/_runtime/common/generic/input.js +1 -0
  13. package/libx/_runtime/common/generic/put.js +1 -0
  14. package/libx/_runtime/common/utils/cqn.js +5 -10
  15. package/libx/_runtime/common/utils/cqn2cqn4sql.js +39 -75
  16. package/libx/_runtime/common/utils/foreignKeyPropagations.js +28 -8
  17. package/libx/_runtime/common/utils/path.js +3 -3
  18. package/libx/_runtime/common/utils/require.js +2 -1
  19. package/libx/_runtime/common/utils/resolveView.js +3 -0
  20. package/libx/_runtime/common/utils/structured.js +6 -1
  21. package/libx/_runtime/db/Service.js +10 -0
  22. package/libx/_runtime/db/expand/expand-v2.js +13 -5
  23. package/libx/_runtime/db/expand/expandCQNToJoin.js +56 -26
  24. package/libx/_runtime/db/utils/generateAliases.js +9 -0
  25. package/libx/_runtime/extensibility/uiflex/handler/transformWRITE.js +1 -0
  26. package/libx/_runtime/fiori/generic/read.js +83 -31
  27. package/libx/_runtime/fiori/generic/readOverDraft.js +22 -13
  28. package/libx/_runtime/fiori/utils/handler.js +3 -0
  29. package/libx/_runtime/fiori/utils/where.js +38 -25
  30. package/libx/_runtime/hana/driver.js +1 -1
  31. package/libx/_runtime/hana/search2cqn4sql.js +4 -1
  32. package/libx/_runtime/remote/Service.js +3 -3
  33. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +19 -12
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@
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.5 - 2022-05-09
8
+
9
+ ### Fixed
10
+
11
+ - `HDB_TCP_KEEP_ALIVE_IDLE` config
12
+ - A combination of `!=` operator and `or` in `where` clauses of `@restrict` annotations or when adjusting `req.query` in custom handlers (OData services only)
13
+ - Programmatic calls to bound actions/functions do have keys in `req.data` again if compat flag `cds.env.features.keys_in_data_compat` is set
14
+
15
+ ## Version 5.9.4 - 2022-05-02
16
+
17
+ ### Fixed
18
+
19
+ - Error messages are improved if no `passport` module was found or if no `xsuaa` service binding is available
20
+ - Issue fixed for `srv.get()`. It was returning `TypeError` in plain REST usage for external services, e.g. `srv.get('/some/arbitrary/path/111')`
21
+ - Allow unrestricted services to run unauthenticated, removing the `Unable to require required package/file "passport"` error. Totally not recommended in production. Note that though this restores pre 5.9.0 behavior, this will come again starting in 6.0.
22
+ - Audit logging of sensitive data in a composition child if its parent is annotated with `@PersonalData.EntitySemantics: 'Other'` and has no data privacy annotations other than `@PersonalData.FieldSemantics: 'DataSubjectID'` annotating a corresponding composition, for example:
23
+ ```js
24
+ annotate Customers with @PersonalData : {
25
+ DataSubjectRole : 'Address',
26
+ EntitySemantics : 'Other'
27
+ } {
28
+ addresses @PersonalData.FieldSemantics: 'DataSubjectID';
29
+ }
30
+ annotate CustomerPostalAddress with @PersonalData : {
31
+ DataSubjectRole : 'Address',
32
+ EntitySemantics : 'DataSubject'
33
+ } {
34
+ ID @PersonalData.FieldSemantics : 'DataSubjectID';
35
+ street @PersonalData.IsPotentiallyPersonal;
36
+ town @PersonalData.IsPotentiallySensitive;
37
+ }
38
+ ```
39
+
40
+ ## Version 5.9.3 - 2022-04-25
41
+
42
+ ### Fixed
43
+
44
+ - 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.
45
+ - 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.
46
+ - 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()`.
47
+ - Invalid batch requests were further processed after error response was already sent to client, leading to an InternalServerError
48
+ - Full support of `SELECT` queries with operator expressions (`xpr`)
49
+
50
+
7
51
  ## Version 5.9.2 - 2022-04-07
8
52
 
9
53
  ### 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] : $ ]
@@ -35,6 +62,25 @@ const add_handler_for = (srv, def) => {
35
62
  }
36
63
  const {params} = target ? target.actions[event] : def
37
64
  if (params) req.data = _named(args,params) || _positional(args,params)
65
+
66
+ // ensure legacy compat, keys in req.data
67
+ if(cds.env.features.keys_in_data_compat && target) {
68
+ // named/positional variant of keys
69
+ const named = req.params.length === 1 && typeof req.params[0] === 'object'
70
+
71
+ let pos = 0 //> counter for key in positional variant
72
+ for (const k in target.keys) {
73
+ if (req.data[k]) {
74
+ LOG._warn && LOG.warn(`
75
+ ${target.name} has defined ${k} as key and action parameter.
76
+ Key will be used in req.data.
77
+ `)
78
+ }
79
+ req.data[k] = named ? req.params[0][k] : req.params[pos++]
80
+ }
81
+
82
+ }
83
+
38
84
  return this.send (req)
39
85
  }
40
86
  stub._is_stub = true
@@ -44,7 +44,10 @@ const _initializers = {
44
44
  // REVISIT: compat, remove with cds^6
45
45
  passport.use(new XSUAAStrategy(uaa.credentials))
46
46
  } else {
47
- throw Object.assign(new Error('No or malformed credentials for auth kind "xsuaa"'), { credentials })
47
+ throw Object.assign(
48
+ new Error('No or malformed credentials for auth kind "xsuaa". Make sure to bind the app to an "xsuaa" service'),
49
+ { credentials }
50
+ )
48
51
  }
49
52
  }
50
53
  }
@@ -178,6 +181,18 @@ module.exports = (srv, app, options) => {
178
181
  // > dummy or mock authentication (for development/testing)
179
182
  _mountMockAuth(srv, app, strategy, config)
180
183
  } else {
184
+ // if no restriction and no binding, don't mount passport middleware
185
+ if (!restricted && !config.credentials) {
186
+ if (!logged) {
187
+ const msg = `Service ${srv.name} is unrestricted`
188
+ if (process.env.NODE_ENV !== 'production') LOG._debug && LOG.debug(msg)
189
+ else LOG._info && LOG.info(`${msg}. This is not recommended in production.`)
190
+ }
191
+
192
+ // no auth wanted > return
193
+ return
194
+ }
195
+
181
196
  // > passport authentication
182
197
  _mountPassportAuth(srv, app, strategy, config)
183
198
  }
@@ -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
  }
@@ -44,7 +44,9 @@ const hasPersonalData = entity => {
44
44
  for (const ele in entity.elements) {
45
45
  if (
46
46
  entity.elements[ele]['@PersonalData.IsPotentiallyPersonal'] ||
47
- entity.elements[ele]['@PersonalData.IsPotentiallySensitive']
47
+ entity.elements[ele]['@PersonalData.IsPotentiallySensitive'] ||
48
+ (entity.elements[ele]['@PersonalData.FieldSemantics'] &&
49
+ entity.elements[ele]['@PersonalData.FieldSemantics'] === 'DataSubjectID')
48
50
  ) {
49
51
  val = true
50
52
  break
@@ -58,7 +60,11 @@ const hasSensitiveData = entity => {
58
60
  let val
59
61
  if (entity['@PersonalData.DataSubjectRole'] && entity['@PersonalData.EntitySemantics']) {
60
62
  for (const ele in entity.elements) {
61
- if (entity.elements[ele]['@PersonalData.IsPotentiallySensitive']) {
63
+ if (
64
+ entity.elements[ele]['@PersonalData.IsPotentiallySensitive'] ||
65
+ (entity.elements[ele]['@PersonalData.FieldSemantics'] &&
66
+ entity.elements[ele]['@PersonalData.FieldSemantics'] === 'DataSubjectID')
67
+ ) {
62
68
  val = true
63
69
  break
64
70
  }
@@ -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`)
@@ -41,6 +41,7 @@ const _getRestrictedExpand = (columns, target, definitions) => {
41
41
  }
42
42
 
43
43
  function handler(req) {
44
+ if (!req.query) return
44
45
  const restricted = _getRestrictedExpand(
45
46
  req.query.SELECT && req.query.SELECT.columns,
46
47
  req.target,
@@ -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) {
@@ -488,16 +487,16 @@ const _convertNotEqual = (container, partName = 'where') => {
488
487
  const where = container[partName]
489
488
 
490
489
  if (where) {
491
- let changed
492
- where.forEach((el, index) => {
490
+ for (let index = 0; index < where.length; index++) {
491
+ const el = where[index]
493
492
  if (el === '!=') {
494
493
  const refIndex = _getRefIndex(where, index)
495
494
  if (refIndex !== undefined) {
496
495
  where[index - 1] = {
497
496
  xpr: [where[index - 1], el, where[index + 1], 'or', where[refIndex], '=', { val: null }]
498
497
  }
499
- where[index] = where[index + 1] = undefined
500
- changed = true
498
+ where.splice(index, 2)
499
+ --index
501
500
  }
502
501
  }
503
502
 
@@ -505,11 +504,6 @@ const _convertNotEqual = (container, partName = 'where') => {
505
504
  if (el.SELECT) _convertNotEqual(el.SELECT, partName)
506
505
  if (el.xpr) _convertNotEqual(el, 'xpr')
507
506
  }
508
- })
509
-
510
- // delete undefined values
511
- if (changed) {
512
- container[partName] = where.filter(el => el)
513
507
  }
514
508
  }
515
509
 
@@ -562,6 +556,10 @@ const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, alias, proc
562
556
  const queryTarget = model.definitions[ensureNoDraftsSuffix(target)]
563
557
 
564
558
  orderByOrWhereCQN.forEach((el, index) => {
559
+ if (el.xpr) {
560
+ _convertOrderByOrWhereCQN(el.xpr, target, model, alias, processFn)
561
+ return
562
+ }
565
563
  if (el.ref && _skip(queryTarget, el.ref, alias, model)) processFn(orderByOrWhereCQN, index)
566
564
  })
567
565
  }
@@ -603,86 +601,56 @@ const _convertRefWhereInExpand = columns => {
603
601
  }
604
602
  }
605
603
 
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)
604
+ const _convertPathExpression = (query, model, options = {}) => {
605
+ const prevAlias = query.SELECT.from.as
606
+ for (const whereEl of query.SELECT.where || []) {
607
+ if (typeof whereEl === 'object' && whereEl.SELECT) _convertPathExpression(whereEl, model)
618
608
  }
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)
631
- }
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)
609
+ const conversion = convertPathExpressionToWhere(query.SELECT.from, model, options)
642
610
  if (!conversion) return
643
611
  const { target, alias, where, cardinality, columns, args } = conversion
644
612
 
645
613
  // REVISIT: It's not possible to just change the reference (i.e. SELECT.from.ref = [target])
646
614
  // as many parts of our code base still refer to SELECT.from (e.g. authorization)
647
615
  if (args) {
648
- SELECT.from = { ref: [{ id: target, args }] }
616
+ query.SELECT.from = { ref: [{ id: target, args }] }
649
617
  } else {
650
- SELECT.from = { ref: [target] }
618
+ query.SELECT.from = { ref: [target] }
651
619
  }
652
620
  if (alias) {
653
- SELECT.from.as = alias
654
- if (SELECT.where && alias !== prevAlias) {
655
- SELECT.where = addAliasToExpression(SELECT.where, alias)
621
+ query.SELECT.from.as = alias
622
+ if (query.SELECT.where && alias !== prevAlias) {
623
+ query.SELECT.where = addAliasToExpression(query.SELECT.where, alias)
656
624
  }
657
625
  }
658
626
  if (columns) {
659
627
  // TODO: use streaming as outer property
660
- if (options.isStreaming) SELECT.columns = columns
628
+ if (options.isStreaming) query.SELECT.columns = columns
661
629
  else {
662
- if (!SELECT.columns) {
630
+ if (!query.SELECT.columns) {
663
631
  // Okra always wants to have the key values, remove once we relax this requirement
664
632
  if (model.definitions[target] && model.definitions[target].keys) {
665
- SELECT.columns = Object.keys(model.definitions[target].keys)
633
+ query.SELECT.columns = Object.keys(model.definitions[target].keys)
666
634
  .filter(
667
635
  k =>
668
636
  !model.definitions[target].keys[k].isAssociation &&
669
637
  !columns.find(element => element.ref && element.ref[element.ref.length - 1] === k)
670
638
  )
671
639
  .map(k => ({ ref: [k] }))
672
- } else SELECT.columns = []
640
+ } else query.SELECT.columns = []
673
641
  }
674
- SELECT.columns.push(...columns)
642
+ query.SELECT.columns.push(...columns)
675
643
  }
676
644
  }
677
645
  if (cardinality && cardinality.max === 1) {
678
- SELECT.one = true
646
+ query.SELECT.one = true
679
647
  }
680
648
  // TODO: REVISIT: We need to add alias to subselect in .where, .columns, .from, ... etc
681
649
  if (where) {
682
650
  if (options._4fiori) {
683
- addToWhere({ SELECT }, where)
651
+ query.where(where)
684
652
  } else {
685
- addToWhere({ SELECT }, removeIsActiveEntityRecursively(where))
653
+ query.where(removeIsActiveEntityRecursively(where))
686
654
  }
687
655
  }
688
656
  }
@@ -693,6 +661,9 @@ const _convertToOneEqNullInFilter = (query, target) => {
693
661
 
694
662
  for (let i = 0; i < query.where.length; i++) {
695
663
  const w = query.where[i]
664
+ if (w.xpr) {
665
+ _convertToOneEqNullInFilter({ where: w.xpr }, target)
666
+ }
696
667
  const w2 = query.where[i + 2]
697
668
  if (!w2 || !w.ref || w2.val !== null) {
698
669
  continue
@@ -716,10 +687,6 @@ const _convertToOneEqNullInFilter = (query, target) => {
716
687
 
717
688
  // eslint-disable-next-line complexity
718
689
  const _convertSelect = (query, model, _options) => {
719
- const { _initial } = _options
720
- if (_initial) {
721
- delete _options._initial
722
- }
723
690
  const options = Object.assign(
724
691
  {
725
692
  _4db: _options.service instanceof cds.DatabaseService,
@@ -744,7 +711,7 @@ const _convertSelect = (query, model, _options) => {
744
711
  _convertNotEqual(query.SELECT, 'having')
745
712
  }
746
713
 
747
- _convertPathExpression(query.SELECT, model, options)
714
+ _convertPathExpression(query, model, options)
748
715
  rewriteAsterisks(query, model, options)
749
716
  if (query.SELECT.where) {
750
717
  const entityName =
@@ -795,9 +762,6 @@ const _convertSelect = (query, model, _options) => {
795
762
  }
796
763
  }
797
764
 
798
- // temporary workaround for xpr - cds v5.9.2 only
799
- if (_initial) _flattenCQN(query)
800
-
801
765
  return query
802
766
  }
803
767
 
@@ -937,7 +901,7 @@ const _convertUpdate = (query, model, options) => {
937
901
  */
938
902
  const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
939
903
  if (query.SELECT) {
940
- return _convertSelect(query, model, Object.assign(options, { _initial: true }))
904
+ return _convertSelect(query, model, options)
941
905
  }
942
906
 
943
907
  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) => {