@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.
- package/CHANGELOG.md +44 -0
- package/lib/compile/for/drafts.js +1 -1
- package/lib/index.js +1 -1
- package/lib/serve/Service-methods.js +47 -1
- package/libx/_runtime/auth/index.js +16 -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/aspects/utils.js +8 -2
- package/libx/_runtime/common/composition/data.js +22 -13
- package/libx/_runtime/common/composition/delete.js +14 -12
- package/libx/_runtime/common/generic/auth/expand.js +1 -0
- 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 +39 -75
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +28 -8
- package/libx/_runtime/common/utils/path.js +3 -3
- package/libx/_runtime/common/utils/require.js +2 -1
- 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/driver.js +1 -1
- package/libx/_runtime/hana/search2cqn4sql.js +4 -1
- package/libx/_runtime/remote/Service.js +3 -3
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +19 -12
- 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
|
-
|
|
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] : $ ]
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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 (
|
|
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
|
|
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) {
|
|
@@ -488,16 +487,16 @@ const _convertNotEqual = (container, partName = 'where') => {
|
|
|
488
487
|
const where = container[partName]
|
|
489
488
|
|
|
490
489
|
if (where) {
|
|
491
|
-
let
|
|
492
|
-
|
|
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
|
|
500
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
651
|
+
query.where(where)
|
|
684
652
|
} else {
|
|
685
|
-
|
|
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
|
|
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,
|
|
904
|
+
return _convertSelect(query, model, options)
|
|
941
905
|
}
|
|
942
906
|
|
|
943
907
|
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) => {
|