@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.
- package/CHANGELOG.md +11 -0
- package/lib/compile/for/drafts.js +1 -1
- package/lib/index.js +1 -1
- package/lib/serve/Service-methods.js +28 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +3 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -3
- package/libx/_runtime/common/composition/data.js +22 -13
- package/libx/_runtime/common/composition/delete.js +14 -12
- package/libx/_runtime/common/generic/input.js +1 -0
- package/libx/_runtime/common/generic/put.js +1 -0
- package/libx/_runtime/common/utils/cqn.js +5 -10
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +35 -66
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +28 -8
- package/libx/_runtime/common/utils/path.js +3 -3
- package/libx/_runtime/common/utils/resolveView.js +3 -0
- package/libx/_runtime/common/utils/structured.js +6 -1
- package/libx/_runtime/db/Service.js +10 -0
- package/libx/_runtime/db/expand/expand-v2.js +13 -5
- package/libx/_runtime/db/expand/expandCQNToJoin.js +56 -26
- package/libx/_runtime/db/utils/generateAliases.js +9 -0
- package/libx/_runtime/extensibility/uiflex/handler/transformWRITE.js +1 -0
- package/libx/_runtime/fiori/generic/read.js +83 -31
- package/libx/_runtime/fiori/generic/readOverDraft.js +22 -13
- package/libx/_runtime/fiori/utils/handler.js +3 -0
- package/libx/_runtime/fiori/utils/where.js +38 -25
- package/libx/_runtime/hana/search2cqn4sql.js +4 -1
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +19 -12
- 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
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 })
|
|
@@ -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(
|
|
244
|
-
if (
|
|
245
|
-
|
|
242
|
+
return where.map(whereEl => {
|
|
243
|
+
if (whereEl.xpr) {
|
|
244
|
+
return { xpr: _addTableName(whereEl.xpr, tableName) }
|
|
246
245
|
}
|
|
247
|
-
|
|
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
|
-
|
|
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
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (
|
|
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
|
-
|
|
656
|
+
query.where(where)
|
|
684
657
|
} else {
|
|
685
|
-
|
|
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
|
|
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,
|
|
909
|
+
return _convertSelect(query, model, options)
|
|
941
910
|
}
|
|
942
911
|
|
|
943
912
|
if (query.UPDATE) {
|
|
@@ -1,24 +1,44 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
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 (
|
|
8
|
-
|
|
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,
|
|
21
|
-
let current = { elements:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
//
|
|
426
|
-
cqn.and(
|
|
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
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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[
|
|
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
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|