@sap/cds 5.6.1 → 5.7.1
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 +136 -0
- package/_i18n/i18n_fr.properties +4 -4
- package/apis/cds.d.ts +7 -10
- package/apis/connect.d.ts +3 -3
- package/apis/core.d.ts +2 -4
- package/apis/models.d.ts +2 -3
- package/apis/ql.d.ts +0 -1
- package/apis/services.d.ts +7 -3
- package/bin/build/buildTaskFactory.js +16 -10
- package/bin/build/buildTaskProviderFactory.js +3 -3
- package/bin/build/constants.js +2 -1
- package/bin/build/provider/buildTaskProviderInternal.js +14 -14
- package/bin/build/provider/hana/2migration.js +2 -3
- package/bin/build/provider/hana/index.js +34 -0
- package/bin/build/provider/hana/migrationtable.js +90 -22
- package/bin/build/provider/hana/template/undeploy.json +5 -0
- package/bin/build/provider/node-cf/index.js +9 -2
- package/bin/serve.js +16 -18
- package/lib/compile/cdsc.js +15 -5
- package/lib/compile/etc/_localized.js +4 -4
- package/lib/compile/extend.js +8 -0
- package/lib/compile/index.js +3 -1
- package/lib/compile/minify.js +61 -0
- package/lib/compile/resolve.js +4 -1
- package/lib/compile/to/gql.js +9 -0
- package/lib/compile/to/sql.js +26 -30
- package/lib/connect/index.js +1 -1
- package/lib/core/entities.js +0 -3
- package/lib/core/infer.js +1 -0
- package/lib/core/reflect.js +1 -34
- package/lib/deploy.js +25 -17
- package/lib/env/defaults.js +3 -1
- package/lib/env/index.js +13 -4
- package/lib/env/presets.js +38 -0
- package/lib/env/requires.js +16 -11
- package/lib/index.js +13 -11
- package/lib/log/format/kibana.js +4 -2
- package/lib/log/index.js +2 -2
- package/lib/ql/Whereable.js +1 -0
- package/lib/req/cds-context.js +79 -0
- package/lib/req/context.js +5 -77
- package/lib/req/request.js +1 -1
- package/lib/serve/Service-api.js +8 -4
- package/lib/serve/Service-dispatch.js +0 -7
- package/lib/serve/Service-methods.js +6 -8
- package/lib/serve/Transaction.js +35 -30
- package/lib/serve/adapters.js +1 -4
- package/lib/utils/axios.js +1 -1
- package/libx/_runtime/audit/Service.js +44 -20
- package/libx/_runtime/audit/generic/personal/access.js +16 -11
- package/libx/_runtime/audit/generic/personal/modification.js +5 -5
- package/libx/_runtime/audit/generic/personal/utils.js +46 -37
- package/libx/_runtime/{common/auth → auth}/index.js +21 -7
- package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
- package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
- package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
- package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +26 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/SetResponseHeadersCommand.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +18 -15
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
- package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
- package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
- package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
- package/libx/_runtime/cds-services/services/Service.js +0 -6
- package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
- package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
- package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
- package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
- package/libx/_runtime/cds-services/util/assert.js +1 -262
- package/libx/_runtime/cds.js +6 -9
- package/libx/_runtime/common/aspects/entity.js +1 -1
- package/libx/_runtime/common/composition/delete.js +4 -2
- package/libx/_runtime/common/composition/update.js +27 -35
- package/libx/_runtime/common/composition/utils.js +3 -7
- package/libx/_runtime/common/error/standardError.js +11 -0
- package/libx/_runtime/common/generic/auth.js +61 -30
- package/libx/_runtime/common/generic/crud.js +11 -23
- package/libx/_runtime/common/generic/input.js +20 -0
- package/libx/_runtime/common/generic/paging.js +2 -2
- package/libx/_runtime/common/generic/put.js +4 -10
- package/libx/_runtime/common/generic/sorting.js +12 -30
- package/libx/_runtime/common/perf/index.js +24 -0
- package/libx/_runtime/common/utils/cqn.js +58 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +289 -114
- package/libx/_runtime/common/utils/csn.js +38 -56
- package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
- package/libx/_runtime/common/utils/resolveView.js +4 -5
- package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
- package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
- package/libx/_runtime/common/utils/structured.js +35 -25
- package/libx/_runtime/db/Service.js +0 -6
- package/libx/_runtime/db/expand/expand-v2.js +130 -0
- package/libx/_runtime/db/expand/expandCQNToJoin.js +82 -61
- package/libx/_runtime/db/expand/index.js +3 -1
- package/libx/_runtime/db/generic/arrayed.js +14 -27
- package/libx/_runtime/db/generic/input.js +52 -10
- package/libx/_runtime/db/generic/integrity.js +367 -26
- package/libx/_runtime/db/generic/virtual.js +51 -13
- package/libx/_runtime/db/query/update.js +9 -3
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
- package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
- package/libx/_runtime/fiori/generic/activate.js +1 -0
- package/libx/_runtime/fiori/generic/before.js +2 -1
- package/libx/_runtime/fiori/generic/edit.js +2 -1
- package/libx/_runtime/fiori/generic/patch.js +1 -1
- package/libx/_runtime/fiori/generic/read.js +151 -57
- package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
- package/libx/_runtime/fiori/uiflex/index.js +1 -1
- package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
- package/libx/_runtime/fiori/utils/delete.js +7 -1
- package/libx/_runtime/hana/Service.js +1 -8
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
- package/libx/_runtime/hana/execute.js +10 -4
- package/libx/_runtime/hana/pool.js +55 -45
- package/libx/_runtime/hana/search.js +7 -6
- package/libx/_runtime/hana/search2cqn4sql.js +8 -5
- package/libx/_runtime/hana/searchToContains.js +3 -1
- package/libx/_runtime/index.js +5 -5
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
- package/libx/_runtime/messaging/Outbox.js +53 -0
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
- package/libx/_runtime/messaging/common-utils/connections.js +14 -9
- package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
- package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
- package/libx/_runtime/messaging/file-based.js +5 -5
- package/libx/_runtime/messaging/message-queuing.js +2 -3
- package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
- package/libx/_runtime/messaging/outbox/utils.js +192 -0
- package/libx/_runtime/messaging/service.js +16 -30
- package/libx/_runtime/remote/Service.js +21 -2
- package/libx/_runtime/remote/utils/client.js +15 -3
- package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
- package/libx/_runtime/sqlite/Service.js +7 -10
- package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
- package/libx/_runtime/sqlite/execute.js +18 -12
- package/libx/_runtime/types/api.js +2 -1
- package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
- package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
- package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +182 -118
- package/libx/odata/index.js +18 -15
- package/libx/odata/parser.js +1 -0
- package/libx/odata/utils.js +57 -0
- package/libx/rest/RestAdapter.js +2 -6
- package/libx/rest/utils/data.js +1 -6
- package/package.json +4 -3
- package/server.js +13 -10
- package/srv/audit-log.cds +87 -0
- package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
- package/srv/flex.js +1 -0
- package/srv/outbox.cds +11 -0
- package/srv/outbox.js +0 -0
- package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
- package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
- package/libx/odata/odata2cqn/index.js +0 -3
- package/libx/odata/odata2cqn/parser.js +0 -1
- package/libx/odata/readme.md +0 -1
- package/libx/odata/utils/index.js +0 -64
|
@@ -6,54 +6,13 @@ const getEtagElement = entity => {
|
|
|
6
6
|
return Object.values(entity.elements).find(element => element['@odata.etag'])
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
const _isDependent = (assoc, parent, target) => {
|
|
10
|
-
return (
|
|
11
|
-
assoc._isAssociationStrict &&
|
|
12
|
-
assoc.is2one &&
|
|
13
|
-
!assoc.on &&
|
|
14
|
-
!parent['@cds.persistence.skip'] &&
|
|
15
|
-
assoc['@assert.integrity'] !== false &&
|
|
16
|
-
parent['@assert.integrity'] !== false &&
|
|
17
|
-
(!parent._service || parent._service['@assert.integrity'] !== false) &&
|
|
18
|
-
!assoc._isCompositionBacklink
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/*
|
|
23
|
-
* this modifies the csn on purpose for caching effect!
|
|
24
|
-
* doing as aspect is difficult due to no global definitons per tenant
|
|
25
|
-
*/
|
|
26
|
-
const getDependents = (entity, model) => {
|
|
27
|
-
if (entity.own('__dependents')) return entity.__dependents
|
|
28
|
-
|
|
29
|
-
/** @type {Array|boolean} */
|
|
30
|
-
let dependents = []
|
|
31
|
-
for (const def of Object.values(model.definitions)) {
|
|
32
|
-
if (def.kind !== 'entity') continue
|
|
33
|
-
if (!def.associations) continue
|
|
34
|
-
|
|
35
|
-
for (const assoc of Object.values(def.associations)) {
|
|
36
|
-
if (assoc.target !== entity.name) continue
|
|
37
|
-
|
|
38
|
-
const parent = assoc.parent
|
|
39
|
-
const target = model.definitions[assoc.target]
|
|
40
|
-
if (_isDependent(assoc, parent, target)) {
|
|
41
|
-
dependents.push({ element: assoc, parent, target })
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (dependents.length === 0) dependents = false
|
|
47
|
-
return entity.set('__dependents', dependents)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
9
|
const _getUps = (entity, model) => {
|
|
51
10
|
const ups = []
|
|
52
11
|
for (const def of Object.values(model.definitions)) {
|
|
53
|
-
if (def.kind !== 'entity') continue
|
|
54
|
-
if (!def.associations) continue
|
|
12
|
+
if (def.kind !== 'entity' || !def.associations) continue
|
|
55
13
|
for (const element of Object.values(def.associations)) {
|
|
56
14
|
if (element.target !== entity.name || element._isBacklink) continue
|
|
15
|
+
if (element.name === 'SiblingEntity') continue
|
|
57
16
|
ups.push(element)
|
|
58
17
|
}
|
|
59
18
|
}
|
|
@@ -64,26 +23,50 @@ const _ifDataSubject = (entity, role) => {
|
|
|
64
23
|
return entity['@PersonalData.EntitySemantics'] === 'DataSubject' && entity['@PersonalData.DataSubjectRole'] === role
|
|
65
24
|
}
|
|
66
25
|
|
|
67
|
-
const _getDataSubjectUp = (role, model,
|
|
68
|
-
const
|
|
69
|
-
|
|
26
|
+
const _getDataSubjectUp = (role, model, entity, prev, next, result) => {
|
|
27
|
+
for (const element of _getUps(entity, model)) {
|
|
28
|
+
const me = { entity, relative: element.parent, element }
|
|
29
|
+
if (prev) prev.next = me
|
|
70
30
|
if (_ifDataSubject(element.parent, role)) {
|
|
71
|
-
|
|
31
|
+
if (!result) result = { dataSubjectEntity: element.parent, subs: [] }
|
|
32
|
+
result.subs.push(next || me)
|
|
33
|
+
return result
|
|
34
|
+
} else {
|
|
35
|
+
// dfs is a must here
|
|
36
|
+
result = _getDataSubjectUp(role, model, element.parent, me, next || me, result)
|
|
72
37
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
38
|
+
}
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const _getDataSubjectDown = (role, entity, prev, next) => {
|
|
43
|
+
const associations = Object.values(entity.associations).filter(e => !e._isBacklink)
|
|
44
|
+
for (const element of associations) {
|
|
45
|
+
const me = { entity, relative: entity, element }
|
|
46
|
+
if (_ifDataSubject(element._target, role)) {
|
|
47
|
+
if (prev) prev.next = me
|
|
48
|
+
return { dataSubjectEntity: element._target, subs: [next || me] }
|
|
78
49
|
}
|
|
79
50
|
}
|
|
51
|
+
// bfs makes more sense here
|
|
52
|
+
for (const element of associations) {
|
|
53
|
+
const me = { entity, relative: entity, element }
|
|
54
|
+
if (prev) prev.next = me
|
|
55
|
+
const dataSubject = _getDataSubjectDown(role, element._target, me, next || me)
|
|
56
|
+
if (dataSubject) return dataSubject
|
|
57
|
+
}
|
|
80
58
|
}
|
|
81
59
|
|
|
82
|
-
const getDataSubject = (entity, model, role
|
|
60
|
+
const getDataSubject = (entity, model, role) => {
|
|
83
61
|
const hash = '__dataSubject4' + role
|
|
84
62
|
if (entity.own(hash)) return entity[hash]
|
|
85
|
-
|
|
86
|
-
|
|
63
|
+
// entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
|
|
64
|
+
// be always below or always above 'DataSubject' entity in CSN tree
|
|
65
|
+
let dataSubject = _getDataSubjectUp(role, model, entity)
|
|
66
|
+
if (!dataSubject) {
|
|
67
|
+
dataSubject = _getDataSubjectDown(role, entity)
|
|
68
|
+
}
|
|
69
|
+
return entity.set(hash, dataSubject)
|
|
87
70
|
}
|
|
88
71
|
|
|
89
72
|
const _resolve = (name, model, namespace) =>
|
|
@@ -204,7 +187,6 @@ module.exports = {
|
|
|
204
187
|
getEtagElement,
|
|
205
188
|
findCsnTargetFor,
|
|
206
189
|
getElementDeep,
|
|
207
|
-
getDependents,
|
|
208
190
|
isRootEntity,
|
|
209
191
|
getDataSubject,
|
|
210
192
|
alias2ref
|
|
@@ -5,26 +5,26 @@ const getEntityNameFromCQN = cqn => {
|
|
|
5
5
|
|
|
6
6
|
// Do the most likely first -> {ref}
|
|
7
7
|
if (cqn.ref) {
|
|
8
|
-
return cqn.ref[0].id || cqn.ref[0]
|
|
8
|
+
return { entityName: cqn.ref[0].id || cqn.ref[0], alias: cqn.as }
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// TODO cleanup
|
|
12
12
|
// REVISIT infer should do this for req.target
|
|
13
13
|
// REVISIT2 No, req.target doesn't make sense for joins
|
|
14
14
|
if (cqn.SET) {
|
|
15
|
-
return cqn.SET.args.map(getEntityNameFromCQN).find(n => n !== 'DRAFT.DraftAdministrativeData')
|
|
15
|
+
return cqn.SET.args.map(getEntityNameFromCQN).find(n => n.entityName !== 'DRAFT.DraftAdministrativeData')
|
|
16
16
|
}
|
|
17
17
|
if (cqn.join) {
|
|
18
|
-
return cqn.args.map(getEntityNameFromCQN).find(n => n !== 'DRAFT.DraftAdministrativeData')
|
|
18
|
+
return cqn.args.map(getEntityNameFromCQN).find(n => n.entityName !== 'DRAFT.DraftAdministrativeData')
|
|
19
19
|
}
|
|
20
|
+
return {}
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
// Note: This also works for the common draft scenarios
|
|
23
24
|
const getEntityFromCQN = (req, service) => {
|
|
24
25
|
if (!req.target || req.target._unresolved) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
return service.model.definitions[ensureNoDraftsSuffix(entity)]
|
|
26
|
+
const { entityName } = getEntityNameFromCQN(req.query)
|
|
27
|
+
return entityName && service.model.definitions[ensureNoDraftsSuffix(entityName)]
|
|
28
28
|
}
|
|
29
29
|
return req.target
|
|
30
30
|
}
|
|
@@ -98,17 +98,16 @@ const _newNestedData = (queryTarget, newData, ref, value) => {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// eslint-disable-next-line complexity
|
|
101
102
|
const _newData = (data, transition, inverse, service) => {
|
|
103
|
+
if (data === null) return null
|
|
102
104
|
// no transition -> nothing to do
|
|
103
105
|
if (transition.target && transition.target.name === transition.queryTarget.name) return data
|
|
104
106
|
|
|
107
|
+
// REVISIT this does not copy deep
|
|
105
108
|
const newData = { ...data }
|
|
106
109
|
const queryTarget = transition.queryTarget
|
|
107
110
|
|
|
108
|
-
/*
|
|
109
|
-
* REVISIT: the current impl results in {} instead of keeping null for compo to one.
|
|
110
|
-
* unfortunately, many follow-up errors occur (e.g., prop in null checks) if changed.
|
|
111
|
-
*/
|
|
112
111
|
for (const key in newData) {
|
|
113
112
|
const el = queryTarget && queryTarget.elements && queryTarget.elements[key]
|
|
114
113
|
const isAssoc = el && el.isAssociation
|
|
@@ -339,7 +338,7 @@ const _newSelect = (query, transitions, service) => {
|
|
|
339
338
|
if (!newSelect.columns && targetTransition.mapping.size) newSelect.columns = _initialColumns(targetTransition)
|
|
340
339
|
if (newSelect.columns) {
|
|
341
340
|
const isDB = service instanceof cds.DatabaseService
|
|
342
|
-
rewriteAsterisks({ SELECT: newSelect },
|
|
341
|
+
rewriteAsterisks({ SELECT: newSelect }, service.model, isDB)
|
|
343
342
|
newSelect.columns = _newColumns(newSelect.columns, targetTransition, service, service.kind !== 'app-service')
|
|
344
343
|
}
|
|
345
344
|
if (newSelect.having) newSelect.having = _newColumns(newSelect.having, targetTransition)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { getNavigationIfStruct } = require('./structured')
|
|
2
2
|
const getColumns = require('../../db/utils/columns')
|
|
3
|
-
const { ensureDraftsSuffix } = require('./draft')
|
|
3
|
+
const { ensureDraftsSuffix, ensureNoDraftsSuffix } = require('./draft')
|
|
4
|
+
const { getEntityNameFromCQN } = require('./entityFromCqn')
|
|
4
5
|
|
|
5
6
|
const isAsteriskColumn = col => col === '*' || (col.ref && col.ref[0] === '*' && !col.expand)
|
|
6
7
|
|
|
@@ -75,15 +76,55 @@ const _rewriteAsterisks = (cqn, target, db, isRoot) => {
|
|
|
75
76
|
return columns
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
const
|
|
79
|
+
const _targetOfQueryIfNotDraft = (query, model) => {
|
|
80
|
+
const { entityName } = getEntityNameFromCQN(query)
|
|
81
|
+
const target = model.definitions[entityName]
|
|
79
82
|
if (!target || target.name.endsWith('_drafts')) return
|
|
80
|
-
|
|
83
|
+
return target
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rewriteAsterisks = (query, model, db = false, isDraft = false, onlyKeys = false) => {
|
|
87
|
+
if (!query.SELECT.columns || !query.SELECT.columns.length) {
|
|
81
88
|
if (isDraft || db) {
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
if (
|
|
90
|
+
query.SELECT.from.SET &&
|
|
91
|
+
query.SELECT.from.SET.args[0] &&
|
|
92
|
+
query.SELECT.from.SET.args[0].SELECT &&
|
|
93
|
+
query.SELECT.from.SET.args[0].SELECT.columns
|
|
94
|
+
) {
|
|
95
|
+
// > best-effort derive column list from first join element if given
|
|
96
|
+
query.SELECT.columns = query.SELECT.from.SET.args[0].SELECT.columns.map(c => ({
|
|
97
|
+
ref: [c.as || c.ref[c.ref.length - 1]]
|
|
98
|
+
}))
|
|
99
|
+
} else if (query.SELECT.from.join && query.SELECT.from.args) {
|
|
100
|
+
if (!query.SELECT.columns) query.SELECT.columns = []
|
|
101
|
+
for (const arg of query.SELECT.from.args) {
|
|
102
|
+
const _targetName = arg.ref[0].id || arg.ref[0]
|
|
103
|
+
const _target = model.definitions[ensureNoDraftsSuffix(_targetName)]
|
|
104
|
+
const columns = getColumns(_target, { db, onlyKeys })
|
|
105
|
+
.filter(
|
|
106
|
+
c =>
|
|
107
|
+
!query.SELECT.columns.some(
|
|
108
|
+
existing => (existing.as || existing.ref[existing.ref.length - 1]) === c.name
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
.map(col => ({
|
|
112
|
+
ref: [arg.as || _targetName, col.name]
|
|
113
|
+
}))
|
|
114
|
+
columns.forEach(c => query.SELECT.columns.push(c))
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const target = _targetOfQueryIfNotDraft(query, model)
|
|
118
|
+
if (!target) return
|
|
119
|
+
query.SELECT.columns = getColumns(target, { db, onlyKeys }).map(col => ({ ref: [col.name] }))
|
|
120
|
+
if (db && target._isDraftEnabled) query.SELECT.columns.push(..._cqlDraftColumns(target))
|
|
121
|
+
}
|
|
84
122
|
}
|
|
85
123
|
return
|
|
86
124
|
}
|
|
125
|
+
const target = _targetOfQueryIfNotDraft(query, model)
|
|
126
|
+
if (!target) return
|
|
127
|
+
// REVISIT: Also support JOINs/SETs here
|
|
87
128
|
query.SELECT.columns = _rewriteAsterisks(query.SELECT, target, db, true)
|
|
88
129
|
}
|
|
89
130
|
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
const { computeColumnsToBeSearched } = require('../../../../libx/_runtime/cds-services/services/utils/columns')
|
|
2
2
|
const searchToLike = require('./searchToLike')
|
|
3
|
+
const { ensureNoDraftsSuffix } = require('./draft')
|
|
4
|
+
const { getEntityNameFromCQN } = require('./entityFromCqn')
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const _targetFrom = (cqn, options) => {
|
|
7
|
+
if (options && options.entityName) return options
|
|
8
|
+
return getEntityNameFromCQN(cqn)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const _search2cqn4sql = (query, model, options = {}) => {
|
|
12
|
+
const cqnSearchPhrase = query.SELECT.search
|
|
13
|
+
if (!cqnSearchPhrase) return
|
|
14
|
+
const { search2cqn4sql } = options
|
|
15
|
+
const { entityName, alias } = _targetFrom(query.SELECT.from, options)
|
|
16
|
+
const entity = model.definitions[ensureNoDraftsSuffix(entityName)]
|
|
17
|
+
const columns = computeColumnsToBeSearched(query, entity, alias)
|
|
10
18
|
|
|
11
19
|
// Call custom (optimized search to cqn for sql implementation) that tries
|
|
12
20
|
// to optimize the search behavior for a specific database service.
|
|
@@ -16,11 +24,15 @@ const search2cqn4sql = (query, model, options) => {
|
|
|
16
24
|
return search2cqn4sql(query, entity, search2cqnOptions)
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
const cqnSearchPhrase = query.SELECT.search
|
|
20
27
|
const expression = searchToLike(cqnSearchPhrase, columns)
|
|
21
28
|
|
|
22
29
|
// REVISIT: find out here if where or having must be used
|
|
23
|
-
query._aggregated ? query.having(expression) : query.where(expression)
|
|
30
|
+
query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
// convert $search system query option to WHERE/HAVING clause using
|
|
34
|
+
// the operator LIKE or CONTAINS
|
|
35
|
+
module.exports = (query, model, options) => {
|
|
36
|
+
if (query.SELECT.from.SET) return query.SELECT.from.SET.args.forEach(arg => _search2cqn4sql(arg, model, options))
|
|
37
|
+
return _search2cqn4sql(query, model, options)
|
|
38
|
+
}
|
|
@@ -204,36 +204,43 @@ const _structFromRef = (ref, csnEntity, model) => {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
const flattenStructuredWhereHaving = (filterArray, csnEntity, model) => {
|
|
207
|
-
if (filterArray)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
}
|
|
207
|
+
if (!filterArray) return
|
|
208
|
+
|
|
209
|
+
const newFilterArray = []
|
|
210
|
+
for (let i = 0; i < filterArray.length; i++) {
|
|
211
|
+
if (OPERATIONS.includes(filterArray[i + 1])) {
|
|
212
|
+
const refElement = filterArray[i].ref ? filterArray[i] : filterArray[i + 2]
|
|
213
|
+
|
|
214
|
+
// copy for processing
|
|
215
|
+
const ref = refElement.ref && refElement.ref.map(ele => ele)
|
|
216
|
+
|
|
217
|
+
// is ref[0] an alias? -> remove
|
|
218
|
+
const isAliased = ref && ref.length > 1 && !csnEntity.elements[ref[0]]
|
|
219
|
+
if (isAliased) ref.shift()
|
|
220
|
+
const { element, idx } = _structFromRef(ref, csnEntity, model)
|
|
221
|
+
|
|
222
|
+
// REVISIT: We cannot make the simple distinction between ref and others
|
|
223
|
+
// for xpr, subselect, we need to call this method recursively
|
|
224
|
+
if (element) {
|
|
225
|
+
if (isAliased) refElement.ref.shift()
|
|
228
226
|
|
|
229
|
-
|
|
227
|
+
// REVISIT: This does not support operator like "between", "in" or a different order of elements like val,op,ref or expressions like ref,op,val+val
|
|
228
|
+
_transformStructToFlatWhereHaving(filterArray.slice(i, i + 3), newFilterArray, element, idx)
|
|
229
|
+
i += 2 // skip next two entries e.g. ('=', '{struct:{int:1}}')
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
230
232
|
}
|
|
231
|
-
|
|
233
|
+
|
|
234
|
+
newFilterArray.push(filterArray[i])
|
|
232
235
|
}
|
|
236
|
+
|
|
237
|
+
return newFilterArray
|
|
233
238
|
}
|
|
239
|
+
|
|
234
240
|
const _entityFromRef = ref => {
|
|
235
241
|
if (ref) return ref[0].id || ref[0]
|
|
236
242
|
}
|
|
243
|
+
|
|
237
244
|
const getNavigationIfStruct = (entity, ref) => {
|
|
238
245
|
const element = entity && entity.elements && entity.elements[_entityFromRef(ref)]
|
|
239
246
|
if (!element) return
|
|
@@ -275,8 +282,11 @@ const flattenStructuredSelect = ({ SELECT }, model) => {
|
|
|
275
282
|
const flattenedElements = []
|
|
276
283
|
const toBeDeleted = []
|
|
277
284
|
_flattenColumns(SELECT, flattenedElements, toBeDeleted, entity)
|
|
278
|
-
SELECT.columns = SELECT.columns.filter(
|
|
279
|
-
|
|
285
|
+
SELECT.columns = SELECT.columns.filter(column => {
|
|
286
|
+
const columnName = column.ref ? column.ref[0] : column.as
|
|
287
|
+
return (columnName && !toBeDeleted.includes(columnName)) || column.func || column.expand
|
|
288
|
+
})
|
|
289
|
+
if (flattenedElements.length) SELECT.columns.push(...flattenedElements)
|
|
280
290
|
}
|
|
281
291
|
if (SELECT.from.args) {
|
|
282
292
|
for (const arg of SELECT.from.args) {
|
|
@@ -38,12 +38,6 @@ class DatabaseService extends cds.Service {
|
|
|
38
38
|
// REVISIT: how to generic handler registration?
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
set model(m) {
|
|
42
|
-
// Ensure the model we get has unfolded entities for localized data, drafts, etc.
|
|
43
|
-
// Note: cds.deploy and some tests set the model of cds.db outside the constructor
|
|
44
|
-
super.model = m && 'definitions' in m ? cds.compile.for.odata(m) : m
|
|
45
|
-
}
|
|
46
|
-
|
|
47
41
|
/*
|
|
48
42
|
* tx
|
|
49
43
|
*/
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const getColumns = require('../utils/columns')
|
|
3
|
+
|
|
4
|
+
const _identifierForRow = (row, prefix, keys) => {
|
|
5
|
+
return keys.map(k => row[`${prefix}${k}`]).join(',')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const _removeParentKeysFromRow = (row, prefix, keys) => {
|
|
9
|
+
for (const k of keys) {
|
|
10
|
+
delete row[`${prefix}${k}`]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const _autoExpandNavsAndAttachToResult = async (entity, previousResult, depth) => {
|
|
15
|
+
for (const nav in entity._associations) {
|
|
16
|
+
const navigation = entity._associations[nav]
|
|
17
|
+
|
|
18
|
+
// do not expand backlinks
|
|
19
|
+
if (navigation._isBacklink) continue
|
|
20
|
+
|
|
21
|
+
const childAlias = 'child'
|
|
22
|
+
const parentAlias = 'parent'
|
|
23
|
+
|
|
24
|
+
const cqnQuery = SELECT.from(`${navigation.target} as ${childAlias}`)
|
|
25
|
+
.join(`${entity.name} as ${parentAlias}`)
|
|
26
|
+
.on(entity._relations[navigation.name].join(childAlias, parentAlias))
|
|
27
|
+
|
|
28
|
+
// set alias for expanded columns already
|
|
29
|
+
const childColumns = getColumns(navigation._target).map(c => ({ ref: [childAlias, c.name] }))
|
|
30
|
+
const parentKeys = Object.keys(entity.keys).filter(k => !entity.keys[k].isAssociation)
|
|
31
|
+
// mark parent key with prefix in alias
|
|
32
|
+
const parentKeysWithAlias = parentKeys.map(pk => ({ ref: [parentAlias, pk], as: `$$pk_${pk}` }))
|
|
33
|
+
cqnQuery.columns(...childColumns, ...parentKeysWithAlias)
|
|
34
|
+
|
|
35
|
+
// add tuple comparison for where clause
|
|
36
|
+
cqnQuery.where([
|
|
37
|
+
{ list: parentKeys.map(pk => ({ ref: [parentAlias, pk] })) },
|
|
38
|
+
'in',
|
|
39
|
+
{ list: previousResult.map(row => ({ list: parentKeys.map(pk => ({ val: row[pk] })) })) }
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
// sort by primary keys of parent, required in future
|
|
43
|
+
cqnQuery.orderBy(parentKeys.map(pk => ({ ref: [parentAlias, pk] })))
|
|
44
|
+
|
|
45
|
+
const result = await cds.db.run(cqnQuery)
|
|
46
|
+
|
|
47
|
+
// TODO: Is there a more efficient/stable way to handle compound keys?
|
|
48
|
+
const map = new Map()
|
|
49
|
+
for (const row of result) {
|
|
50
|
+
const identifier = _identifierForRow(row, '$$pk_', parentKeys)
|
|
51
|
+
_removeParentKeysFromRow(row, '$$pk_', parentKeys)
|
|
52
|
+
if (map.has(identifier)) {
|
|
53
|
+
map.get(identifier).push(row)
|
|
54
|
+
} else {
|
|
55
|
+
map.set(identifier, [row])
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// link previous result with current result
|
|
60
|
+
previousResult.forEach(row => {
|
|
61
|
+
const identifier = _identifierForRow(row, '', parentKeys)
|
|
62
|
+
if (map.has(identifier)) {
|
|
63
|
+
const entry = map.get(identifier)
|
|
64
|
+
row[nav] = navigation.is2one ? entry[0] : entry
|
|
65
|
+
} else {
|
|
66
|
+
row[nav] = navigation.is2one ? null : []
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// expand next level if needed
|
|
71
|
+
if (depth - 1 !== 0 && result.length && navigation._target._associations) {
|
|
72
|
+
await _autoExpandNavsAndAttachToResult(navigation._target, result, depth - 1)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return previousResult
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const _foreignKeysOfTopLevelNavs = entity => {
|
|
80
|
+
const requiredFks = new Set()
|
|
81
|
+
for (const nav in entity._associations) {
|
|
82
|
+
const onCond = entity._relations[nav].join('child', 'parent')
|
|
83
|
+
for (const ele of onCond) {
|
|
84
|
+
if (ele.ref && ele.ref[0] === 'parent') {
|
|
85
|
+
requiredFks.add(ele.ref.slice(1).join('_'))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return [...requiredFks]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 1. Creates flattened SQL statements for each expand layer
|
|
94
|
+
* 2. Mixes in foreign keys if needed
|
|
95
|
+
* 3. Merges results
|
|
96
|
+
* 4. Cleans result if needed
|
|
97
|
+
*
|
|
98
|
+
* @returns object
|
|
99
|
+
*/
|
|
100
|
+
const expandV2 = async (model, dbc, query, user, locale, txTimestamp, executeSelectCQN) => {
|
|
101
|
+
// remove expand columns from query without modifying
|
|
102
|
+
const topLevelSelect = query.clone().columns(query.SELECT.columns.filter(c => !c.expand))
|
|
103
|
+
|
|
104
|
+
const entity = model.definitions[topLevelSelect.SELECT.from.ref[0]]
|
|
105
|
+
|
|
106
|
+
// ensure foreign keys are selected if needed
|
|
107
|
+
const fks = _foreignKeysOfTopLevelNavs(entity)
|
|
108
|
+
fks.forEach(fk => {
|
|
109
|
+
if (!topLevelSelect.SELECT.columns.some(c => c.ref[0] === fk)) {
|
|
110
|
+
topLevelSelect.SELECT.columns.push({ ref: [fk] })
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const result = await executeSelectCQN(model, dbc, topLevelSelect, user, locale, txTimestamp)
|
|
115
|
+
|
|
116
|
+
if (!result || (Array.isArray(result) && !result.length)) {
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// _associations contains compositions and associations
|
|
121
|
+
if (entity._associations) {
|
|
122
|
+
const expandColumn = query.SELECT.columns.find(c => c.expand && typeof c.expand === 'string')
|
|
123
|
+
const depth = expandColumn.expand === '**' ? -1 : Number(expandColumn.expand.replace('*', ''))
|
|
124
|
+
await _autoExpandNavsAndAttachToResult(entity, Array.isArray(result) ? result : [result], depth)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = expandV2
|