@sap/cds 5.7.2 → 5.8.0
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 +108 -0
- package/app/fiori/routes.js +1 -1
- package/bin/deploy/to-hana/cfUtil.js +251 -138
- package/bin/deploy/to-hana/gitUtil.js +55 -0
- package/bin/deploy/to-hana/hana.js +92 -93
- package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
- package/bin/deploy/to-hana/index.js +14 -13
- package/bin/mtx/in-cds.js +1 -0
- package/bin/serve.js +1 -1
- package/bin/version.js +1 -0
- package/lib/compile/cdsc.js +0 -6
- package/lib/compile/minify.js +1 -1
- package/lib/compile/resolve.js +1 -1
- package/lib/compile/to/srvinfo.js +1 -1
- package/lib/core/classes.js +21 -1
- package/lib/env/index.js +3 -2
- package/lib/env/requires.js +4 -0
- package/lib/i18n/localize.js +5 -8
- package/lib/index.js +1 -0
- package/lib/log/errors.js +1 -1
- package/lib/ql/SELECT.js +2 -2
- package/lib/req/cds-context.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/serve/Transaction.js +9 -5
- package/lib/serve/index.js +13 -21
- package/lib/utils/tests.js +90 -66
- package/libx/_runtime/audit/generic/personal/modification.js +0 -8
- package/libx/_runtime/auth/index.js +7 -6
- package/libx/_runtime/auth/strategies/dwc.js +43 -0
- package/libx/_runtime/auth/utils.js +24 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -38
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -42
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +18 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +7 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +21 -30
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +5 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +18 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +80 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
- package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
- package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
- package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
- package/libx/_runtime/cds-services/services/Service.js +1 -1
- package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
- package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
- package/libx/_runtime/common/aspects/Association.js +16 -0
- package/libx/_runtime/common/composition/data.js +28 -37
- package/libx/_runtime/common/composition/delete.js +107 -58
- package/libx/_runtime/common/composition/index.js +2 -1
- package/libx/_runtime/common/composition/insert.js +13 -13
- package/libx/_runtime/common/composition/update.js +39 -34
- package/libx/_runtime/common/error/frontend.js +17 -2
- package/libx/_runtime/common/generic/auth.js +20 -85
- package/libx/_runtime/common/generic/crud.js +22 -1
- package/libx/_runtime/common/i18n/messages.properties +3 -0
- package/libx/_runtime/common/utils/cqn.js +2 -6
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +97 -123
- package/libx/_runtime/common/utils/csn.js +14 -3
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
- package/libx/_runtime/common/utils/keys.js +2 -1
- package/libx/_runtime/common/utils/path.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +12 -4
- package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
- package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
- package/libx/_runtime/common/utils/structured.js +1 -1
- package/libx/_runtime/common/utils/vcap.js +27 -10
- package/libx/_runtime/db/data-conversion/post-processing.js +42 -35
- package/libx/_runtime/db/expand/expand-v2.js +21 -12
- package/libx/_runtime/db/expand/expandCQNToJoin.js +27 -29
- package/libx/_runtime/db/expand/index.js +3 -0
- package/libx/_runtime/db/generic/create.js +0 -10
- package/libx/_runtime/db/generic/index.js +3 -0
- package/libx/_runtime/db/generic/read.js +2 -24
- package/libx/_runtime/db/generic/rewrite.js +1 -3
- package/libx/_runtime/db/generic/update.js +1 -1
- package/libx/_runtime/db/query/delete.js +10 -4
- package/libx/_runtime/db/query/insert.js +3 -3
- package/libx/_runtime/db/query/read.js +15 -8
- package/libx/_runtime/db/query/update.js +5 -5
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
- package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
- package/libx/_runtime/db/sql-builder/index.js +3 -0
- package/libx/_runtime/db/utils/columns.js +5 -2
- package/libx/_runtime/db/utils/deep.js +6 -8
- package/libx/_runtime/db/utils/generateAliases.js +56 -6
- package/libx/_runtime/fiori/generic/before.js +73 -49
- package/libx/_runtime/fiori/generic/edit.js +14 -18
- package/libx/_runtime/fiori/generic/patch.js +8 -11
- package/libx/_runtime/fiori/generic/read.js +22 -17
- package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/conversion.js +10 -0
- package/libx/_runtime/hana/execute.js +33 -16
- package/libx/_runtime/hana/localized.js +1 -1
- package/libx/_runtime/hana/search.js +3 -3
- package/libx/_runtime/hana/search2cqn4sql.js +22 -21
- package/libx/_runtime/hana/searchToContains.js +1 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
- package/libx/_runtime/messaging/file-based.js +3 -1
- package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
- package/libx/_runtime/messaging/service.js +16 -7
- package/libx/_runtime/remote/utils/client.js +33 -20
- package/libx/_runtime/remote/utils/data.js +53 -12
- package/libx/_runtime/sqlite/Service.js +1 -1
- package/libx/_runtime/sqlite/conversion.js +10 -0
- package/libx/_runtime/sqlite/localized.js +1 -1
- package/libx/_runtime/types/api.js +2 -2
- package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
- package/libx/odata/afterburner.js +29 -6
- package/libx/odata/cqn2odata.js +9 -0
- package/libx/odata/grammar.pegjs +101 -45
- package/libx/odata/index.js +7 -1
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +2 -2
- package/libx/rest/RestAdapter.js +29 -1
- package/libx/rest/middleware/auth.js +1 -3
- package/libx/rest/middleware/parse.js +1 -0
- package/package.json +1 -1
- package/server.js +1 -1
- package/bin/deploy/to-hana/logger.js +0 -27
- package/bin/deploy/to-hana/runCommand.js +0 -113
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
- package/libx/_runtime/common/utils/auth.js +0 -16
|
@@ -10,7 +10,6 @@ const {
|
|
|
10
10
|
} = require('../utils/handler')
|
|
11
11
|
const { getKeysCondition } = require('../utils/where')
|
|
12
12
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
13
|
-
|
|
14
13
|
const { DRAFT_COLUMNS_CQN } = require('../../common/constants/draft')
|
|
15
14
|
|
|
16
15
|
const _getSelectCQN = (model, { data, target: { name } }, keysCondition, checkUser = true) => {
|
|
@@ -24,6 +23,7 @@ const _getSelectCQN = (model, { data, target: { name } }, keysCondition, checkUs
|
|
|
24
23
|
),
|
|
25
24
|
...DRAFT_COLUMNS_CQN
|
|
26
25
|
]
|
|
26
|
+
|
|
27
27
|
if (checkUser) {
|
|
28
28
|
columns.push({ ref: ['DRAFT.DraftAdministrativeData', 'inProcessByUser'], as: 'draftAdmin_inProcessByUser' })
|
|
29
29
|
}
|
|
@@ -42,14 +42,16 @@ const _getSelectCQN = (model, { data, target: { name } }, keysCondition, checkUs
|
|
|
42
42
|
|
|
43
43
|
const _getUpdateDraftCQN = ({ query, target: { name } }, keysCondition) => {
|
|
44
44
|
const set = {}
|
|
45
|
+
|
|
45
46
|
for (const entry in query.UPDATE.data) {
|
|
46
47
|
if (entry === 'DraftAdministrativeData_DraftUUID') {
|
|
47
48
|
continue
|
|
48
49
|
}
|
|
50
|
+
|
|
49
51
|
set[entry] = query.UPDATE.data[entry]
|
|
50
52
|
}
|
|
51
|
-
if (set.IsActiveEntity) set.IsActiveEntity = false
|
|
52
53
|
|
|
54
|
+
if (set.IsActiveEntity) set.IsActiveEntity = false
|
|
53
55
|
return UPDATE(ensureDraftsSuffix(name)).data(set).where(keysCondition)
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -65,9 +67,7 @@ const _handler = async function (req) {
|
|
|
65
67
|
if (req.data.IsActiveEntity === 'true') req.reject(400, 'Patch can only be applied to a draft entity')
|
|
66
68
|
|
|
67
69
|
const keysCondition = getKeysCondition(req.target, req.data)
|
|
68
|
-
|
|
69
70
|
const dbtx = cds.tx(req)
|
|
70
|
-
|
|
71
71
|
let result = await dbtx.run(_getSelectCQN(this.model, req, keysCondition))
|
|
72
72
|
|
|
73
73
|
// Potential timeout scenario supported
|
|
@@ -80,19 +80,16 @@ const _handler = async function (req) {
|
|
|
80
80
|
const updateDraftAdminCQN = getUpdateDraftAdminCQN(req, result[0].DraftAdministrativeData_DraftUUID)
|
|
81
81
|
|
|
82
82
|
await Promise.all([dbtx.run(updateDraftCQN), dbtx.run(updateDraftAdminCQN)])
|
|
83
|
-
|
|
84
83
|
result = await dbtx.run(_getSelectCQN(this.model, req, keysCondition, false))
|
|
85
|
-
if (result.length === 0)
|
|
86
|
-
req.reject(404)
|
|
87
|
-
}
|
|
88
|
-
|
|
84
|
+
if (result.length === 0) req.reject(404)
|
|
89
85
|
removeDraftUUIDIfNecessary(result[0], req)
|
|
90
|
-
|
|
91
86
|
return result[0]
|
|
92
87
|
}
|
|
93
88
|
|
|
94
89
|
module.exports = cds.service.impl(function () {
|
|
95
|
-
|
|
90
|
+
const entities = Object.values(this.entities).filter(e => e._isDraftEnabled)
|
|
91
|
+
|
|
92
|
+
for (const entity of entities) {
|
|
96
93
|
this.on('PATCH', entity, _handler)
|
|
97
94
|
}
|
|
98
95
|
})
|
|
@@ -50,7 +50,8 @@ const _getWhereWithAppendedDraftRestrictions = (where = [], req, scenarioAlias,
|
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
if (where.length) where.push('and')
|
|
53
|
-
|
|
53
|
+
// restriction might contain or clause -> use xpr for grouping
|
|
54
|
+
xpr.includes('or') ? where.push({ xpr }) : where.push(...xpr)
|
|
54
55
|
} else {
|
|
55
56
|
// > restriction inherited from parent via autoexposure
|
|
56
57
|
// find inner most sub select if available and append restriction to where clause
|
|
@@ -778,13 +779,16 @@ const _getDraftDoc = (req, draftName, draftWhere) => {
|
|
|
778
779
|
return draftDocs
|
|
779
780
|
}
|
|
780
781
|
|
|
781
|
-
const _getOrderByEnrichedColumns = (orderBy, columns) => {
|
|
782
|
+
const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
|
|
782
783
|
const enrichedCol = []
|
|
783
784
|
if (orderBy && orderBy.length > 1) {
|
|
784
785
|
const colNames = columns.map(el => el.ref[el.ref.length - 1])
|
|
785
786
|
// REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
|
|
786
787
|
for (const el of orderBy) {
|
|
787
|
-
|
|
788
|
+
// For associations we need to 'materialise' the resulting field, otherwise we cannot access it in an outer SELECT.
|
|
789
|
+
if (entity && entity.elements[el.ref[0]] && entity.elements[el.ref[0]].isAssociation) {
|
|
790
|
+
enrichedCol.push({ ref: [...el.ref], as: _poorMansAlias4(el) })
|
|
791
|
+
} else if (!DRAFT_COLUMNS.includes(el.ref[el.ref.length - 1]) && !colNames.includes(el.ref[el.ref.length - 1])) {
|
|
788
792
|
enrichedCol.push({ ref: [...el.ref] })
|
|
789
793
|
}
|
|
790
794
|
}
|
|
@@ -806,11 +810,11 @@ const _replaceDraftAlias = where => {
|
|
|
806
810
|
|
|
807
811
|
const _poorMansAlias4 = xpr => '_' + xpr.ref.join('_') + '_'
|
|
808
812
|
|
|
809
|
-
const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) => {
|
|
813
|
+
const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model, entity) => {
|
|
810
814
|
const draftActiveWhere = _getWhereForActive(draftWhere)
|
|
811
815
|
const activeDocs = getEnrichedCQN(SELECT.from(req.target), req.query.SELECT, draftActiveWhere, undefined, false)
|
|
812
816
|
activeDocs.where(_getWhereWithAppendedDraftRestrictions([], req))
|
|
813
|
-
convertWhereExists(activeDocs, model, {})
|
|
817
|
+
convertWhereExists(activeDocs.SELECT, model, {})
|
|
814
818
|
|
|
815
819
|
// @restrict.where not applicable for drafts (I can ALWAYS read mine)
|
|
816
820
|
_replaceDraftAlias(draftWhere)
|
|
@@ -818,6 +822,7 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) =>
|
|
|
818
822
|
|
|
819
823
|
const union = SELECT.from({ SET: { op: 'union', all: true, args: [draftDocs, activeDocs] } })
|
|
820
824
|
if (req.query.SELECT.count) union.SELECT.count = true
|
|
825
|
+
if (req.query.SELECT.__countAggregated) union.SELECT.__countAggregated = true
|
|
821
826
|
|
|
822
827
|
if (req.query.SELECT.from.as) {
|
|
823
828
|
draftDocs.SELECT.from.as = req.query.SELECT.from.as
|
|
@@ -836,7 +841,7 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) =>
|
|
|
836
841
|
return union.columns({ func: 'sum', args: [{ ref: ['$count'] }], as: '$count' })
|
|
837
842
|
}
|
|
838
843
|
|
|
839
|
-
const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns)
|
|
844
|
+
const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns, entity)
|
|
840
845
|
|
|
841
846
|
for (const col of enrichedColumns) {
|
|
842
847
|
// if we have columns for outer order by that may also be needed for joins, we need to duplicate them
|
|
@@ -913,12 +918,14 @@ const _excludeActiveDraftExists = (req, draftWhere, columns, model) => {
|
|
|
913
918
|
])
|
|
914
919
|
.where(_inProcessByUserWhere(req.user.id))
|
|
915
920
|
|
|
921
|
+
const targetName = ensureNoDraftsSuffix(req.target.name)
|
|
916
922
|
for (const key of _getTargetKeys(req)) {
|
|
917
|
-
subSelect.where([{ ref: [
|
|
923
|
+
subSelect.where([{ ref: [targetName, key] }, '=', { ref: [draftName, key] }])
|
|
918
924
|
}
|
|
919
925
|
|
|
926
|
+
const entity = model.definitions[targetName]
|
|
920
927
|
draftWhere = removeIsActiveEntityRecursively(draftWhere)
|
|
921
|
-
const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model)
|
|
928
|
+
const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model, entity)
|
|
922
929
|
cqn.SELECT.from.as = name
|
|
923
930
|
|
|
924
931
|
if (cqn.SELECT.orderBy) {
|
|
@@ -958,13 +965,16 @@ const _validatedWithSiblingInProcess = (req, draftWhere, draftParameters, column
|
|
|
958
965
|
)
|
|
959
966
|
return _excludeActiveDraftExists(req, draftWhere, columns, model)
|
|
960
967
|
if (
|
|
968
|
+
draftInProcessByUser &&
|
|
961
969
|
draftInProcessByUser.op === '!=' &&
|
|
962
970
|
_isValidWithDraftLocked(isActiveEntity, siblingIsActive, draftInProcessByUser)
|
|
963
971
|
) {
|
|
964
972
|
return _activeWithDraftInProcess(req, draftWhere, columns, req.user.id)
|
|
965
|
-
} else if (_isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
|
|
973
|
+
} else if (draftInProcessByUser && _isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
|
|
966
974
|
return _activeWithDraftInProcess(req, draftWhere, columns, null)
|
|
967
975
|
}
|
|
976
|
+
|
|
977
|
+
//
|
|
968
978
|
}
|
|
969
979
|
|
|
970
980
|
const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, columns) =>
|
|
@@ -1213,16 +1223,13 @@ const _handler = async function (req) {
|
|
|
1213
1223
|
// handle localized here as it was previously handled for req.target
|
|
1214
1224
|
req.target = _getLocalizedEntity(this.model, req.target, req.user) || req.target
|
|
1215
1225
|
|
|
1216
|
-
// REVISIT
|
|
1217
|
-
delete req.query._validationQuery
|
|
1218
|
-
|
|
1219
1226
|
const originalFrom = _copyCQNPartial(req.query.SELECT.from)
|
|
1220
1227
|
|
|
1221
1228
|
// REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
|
|
1222
|
-
const
|
|
1229
|
+
const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
|
|
1223
1230
|
|
|
1224
1231
|
// do not clone with Object.assign as that would skip all non-enumerable properties
|
|
1225
|
-
const reqClone = { __proto__: req, query: _copyCQNPartial(
|
|
1232
|
+
const reqClone = { __proto__: req, query: _copyCQNPartial(query4sql) }
|
|
1226
1233
|
|
|
1227
1234
|
// ensure draft restrictions are copied to new query
|
|
1228
1235
|
reqClone.query._draftRestrictions = req.query._draftRestrictions
|
|
@@ -1232,6 +1239,7 @@ const _handler = async function (req) {
|
|
|
1232
1239
|
reqClone.query._streaming = true
|
|
1233
1240
|
return cds.tx(req).run(reqClone.query)
|
|
1234
1241
|
}
|
|
1242
|
+
|
|
1235
1243
|
let cqnScenario
|
|
1236
1244
|
|
|
1237
1245
|
// to replace scenario CQNs for queries with $apply SELECT chain (new odata2cqn parser)
|
|
@@ -1262,16 +1270,13 @@ const _handler = async function (req) {
|
|
|
1262
1270
|
)
|
|
1263
1271
|
|
|
1264
1272
|
_adaptSubSelects(cqnScenario.cqn, cqnScenario.scenario)
|
|
1265
|
-
|
|
1266
1273
|
_adaptAnnotationAliases(cqnScenario.cqn)
|
|
1267
1274
|
|
|
1268
1275
|
// unlocalize for db and after handlers as it was before
|
|
1269
1276
|
req.target = this.model.definitions[ensureUnlocalized(req.target.name)]
|
|
1270
1277
|
|
|
1271
1278
|
const result = await cds.tx(req).send({ query: cqnScenario.cqn, target: req.target })
|
|
1272
|
-
|
|
1273
1279
|
const resultAsArray = Array.isArray(result) ? result : result ? [result] : []
|
|
1274
|
-
|
|
1275
1280
|
removeDraftUUIDIfNecessary(resultAsArray, req)
|
|
1276
1281
|
|
|
1277
1282
|
if (cqnScenario.scenario === SCENARIO.DRAFT_ADMIN) {
|
|
@@ -45,7 +45,7 @@ const _handler = async function (req) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
|
|
48
|
-
const sqlQuery = cqn2cqn4sql(req.query, this.model, {
|
|
48
|
+
const sqlQuery = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
|
|
49
49
|
if (req.query._streaming) {
|
|
50
50
|
sqlQuery._streaming = true
|
|
51
51
|
}
|
|
@@ -53,9 +53,6 @@ const _handler = async function (req) {
|
|
|
53
53
|
const hasDraftEntity = hasDraft(this.model.definitions, sqlQuery)
|
|
54
54
|
|
|
55
55
|
if (hasDraftEntity && sqlQuery.SELECT.where && sqlQuery.SELECT.where.length !== 0) {
|
|
56
|
-
// REVISIT
|
|
57
|
-
delete req.query._validationQuery
|
|
58
|
-
|
|
59
56
|
let cqnDraft = SELECT.from({
|
|
60
57
|
ref: [...sqlQuery.SELECT.from.ref],
|
|
61
58
|
as: sqlQuery.SELECT.from.as
|
|
@@ -41,7 +41,7 @@ class HanaDatabase extends DatabaseService {
|
|
|
41
41
|
this._insert = this._queries.insert(execute.insert)
|
|
42
42
|
this._read = this._queries.read(execute.select, execute.stream)
|
|
43
43
|
this._update = this._queries.update(execute.update, execute.select)
|
|
44
|
-
this._delete = this._queries.delete(execute.delete)
|
|
44
|
+
this._delete = this._queries.delete(execute.delete, execute.update)
|
|
45
45
|
this._run = this._queries.run(this._insert, this._read, this._update, this._delete, execute.cqn, execute.sql)
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
2
|
+
|
|
1
3
|
const convertToBoolean = boolean => {
|
|
2
4
|
if (boolean === null) {
|
|
3
5
|
return null
|
|
@@ -47,6 +49,14 @@ const HANA_TYPE_CONVERSION_MAP = new Map([
|
|
|
47
49
|
['cds.LargeString', convertToString]
|
|
48
50
|
])
|
|
49
51
|
|
|
52
|
+
if (cds.env.features.bigjs) {
|
|
53
|
+
const Big = require('big.js')
|
|
54
|
+
const convertToBig = value => new Big(value)
|
|
55
|
+
|
|
56
|
+
HANA_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
|
|
57
|
+
HANA_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
module.exports = {
|
|
51
61
|
HANA_TYPE_CONVERSION_MAP
|
|
52
62
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
2
|
+
const LOG = cds.log('hana|db|sql')
|
|
3
|
+
|
|
1
4
|
const { HANA_TYPE_CONVERSION_MAP } = require('./conversion')
|
|
2
5
|
const CustomBuilder = require('./customBuilder')
|
|
3
6
|
const { sqlFactory } = require('../db/sql-builder/')
|
|
@@ -30,9 +33,6 @@ function _cqnToSQL(model, query, user, locale, txTimestamp) {
|
|
|
30
33
|
)
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
const cds = require('../cds')
|
|
34
|
-
const LOG = cds.log('hana|db|sql')
|
|
35
|
-
|
|
36
36
|
const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
37
37
|
|
|
38
38
|
function _getOutputParameters(stmt) {
|
|
@@ -48,6 +48,26 @@ function _getOutputParameters(stmt) {
|
|
|
48
48
|
return Object.keys(result).length > 0 ? result : undefined
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
const BINARY_TYPES = {
|
|
52
|
+
12: 'BINARY',
|
|
53
|
+
13: 'VARBINARY',
|
|
54
|
+
25: 'CLOB',
|
|
55
|
+
26: 'NCLOB',
|
|
56
|
+
27: 'BLOB'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _getBinaries(stmt) {
|
|
60
|
+
// hdb vs. @sap/hana-client
|
|
61
|
+
const parameters = stmt.parameterMetadata || stmt.getParameterInfo()
|
|
62
|
+
const typeKey = stmt.parameterMetadata ? 'dataType' : 'nativeType'
|
|
63
|
+
return parameters.reduce((acc, cur, i) => {
|
|
64
|
+
if (BINARY_TYPES[cur[typeKey]]) acc.push(i)
|
|
65
|
+
return acc
|
|
66
|
+
}, [])
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
|
|
70
|
+
|
|
51
71
|
function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
52
72
|
dbc.prepare(sql, function (err, stmt) {
|
|
53
73
|
if (err) {
|
|
@@ -56,18 +76,15 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
|
56
76
|
return reject(err)
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
//
|
|
60
|
-
if (
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (stmt.parameterMetadata[i].dataType === 12 || stmt.parameterMetadata[i].dataType === 13) {
|
|
69
|
-
if (row[i] && !Buffer.isBuffer(row[i])) {
|
|
70
|
-
row[i] = Buffer.from(row[i].match(/.{1,2}/g).map(val => parseInt(val, 16)))
|
|
79
|
+
// convert binary strings to buffers ()
|
|
80
|
+
if (cds.env.hana.base64_to_buffer !== false && _hasValues(values)) {
|
|
81
|
+
const binaries = _getBinaries(stmt)
|
|
82
|
+
if (binaries.length) {
|
|
83
|
+
const vals = Array.isArray(values[0]) ? values : [values]
|
|
84
|
+
for (const i of binaries) {
|
|
85
|
+
for (const row of vals) {
|
|
86
|
+
if (row[i] && typeof row[i] === 'string' && row[i].match(BASE64)) {
|
|
87
|
+
row[i] = Buffer.from(row[i], 'base64')
|
|
71
88
|
}
|
|
72
89
|
}
|
|
73
90
|
}
|
|
@@ -108,7 +125,7 @@ function _executeSimpleSQL(dbc, sql, values) {
|
|
|
108
125
|
values = Object.values(values)
|
|
109
126
|
}
|
|
110
127
|
// ensure that stored procedure with parameters is always executed as prepared
|
|
111
|
-
if (_hasValues(
|
|
128
|
+
if (_hasValues(values) || sql.match(/^call.*?\?.*$/i)) {
|
|
112
129
|
_executeAsPreparedStatement(dbc, sql, values, reject, resolve)
|
|
113
130
|
} else {
|
|
114
131
|
dbc.exec(sql, function (err, result, procedureReturn) {
|
|
@@ -31,7 +31,7 @@ const localizedHandler = function (req) {
|
|
|
31
31
|
// suppress localization in "select for update"
|
|
32
32
|
if (query.SELECT.forUpdate) return
|
|
33
33
|
|
|
34
|
-
redirect(query
|
|
34
|
+
redirect(query, getLocalize(req.locale, this.model))
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
localizedHandler._initial = true
|
|
@@ -11,9 +11,9 @@ function searchHandler(req) {
|
|
|
11
11
|
// REVISIT: remove feature toggle optimized_search after grace period
|
|
12
12
|
// inject the search2cqn4sql module into the rewrite handler only when
|
|
13
13
|
// the optimized search feature toggle is turned on
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
if (cds.env.features.optimized_search) {
|
|
15
|
+
_setSearchOptions(req.query, { search2cqn4sql, locale: req.locale })
|
|
16
|
+
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// handlers marked with `._initial = true` run in sequence
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { computeColumnsToBeSearched } = require('../cds-services/services/utils/columns')
|
|
2
2
|
const searchToLike = require('../common/utils/searchToLike')
|
|
3
3
|
const { isContainsPredicateSupported, searchToContains } = require('./searchToContains')
|
|
4
|
+
const { addAliasToExpression } = require('../db/utils/generateAliases')
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Computes a CQN expression for a search query.
|
|
@@ -33,8 +34,7 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
33
34
|
// suppress the localize handler from redirecting the query's target to the localized view
|
|
34
35
|
Object.defineProperty(query, '_suppressLocalization', { value: true })
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
if (resolveLocalizedDataAtRuntime && !query.SELECT.from.SELECT) {
|
|
37
|
+
if (resolveLocalizedDataAtRuntime) {
|
|
38
38
|
const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
|
|
39
39
|
|
|
40
40
|
// replace $user_locale placeholder with the user locale or the HANA session context
|
|
@@ -46,22 +46,27 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
46
46
|
query.join(localizedEntityName).on(onCondition)
|
|
47
47
|
|
|
48
48
|
// prevent SQL ambiguity error for columns with the same name
|
|
49
|
-
columnsToBeSearched =
|
|
49
|
+
columnsToBeSearched = _addAliasToQuery(query, entity, columnsToBeSearched)
|
|
50
50
|
} // else --> resolve localized texts via localized view (default)
|
|
51
51
|
|
|
52
52
|
const useContains = isContainsPredicateSupported(query)
|
|
53
53
|
let expression
|
|
54
54
|
|
|
55
55
|
if (useContains) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const namedColumns = columnsToBeSearched.filter(col => !col.func)
|
|
57
|
+
expression = searchToContains(cqnSearchPhrase, namedColumns)
|
|
58
|
+
|
|
59
|
+
// func columns handling
|
|
60
|
+
if (columnsToBeSearched.length > namedColumns.length) {
|
|
61
|
+
const funcColumns = columnsToBeSearched.filter(col => col.func)
|
|
62
|
+
expression = [expression, 'or', ...searchToLike(cqnSearchPhrase, funcColumns)]
|
|
63
|
+
}
|
|
60
64
|
} else {
|
|
61
65
|
// No CONTAINS optimization possible. The search implementation for localized
|
|
62
66
|
// texts falls back to the LIKE predicate.
|
|
63
67
|
expression = searchToLike(cqnSearchPhrase, columnsToBeSearched)
|
|
64
68
|
}
|
|
69
|
+
|
|
65
70
|
// REVISIT: find out here if where or having must be used
|
|
66
71
|
query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
|
|
67
72
|
return query
|
|
@@ -75,27 +80,23 @@ const _getLocalizedAssociation = entity => {
|
|
|
75
80
|
// The inner join modifies the original SELECT ... FROM query and adds ambiguity,
|
|
76
81
|
// therefore add the table/entity name (as a preceding element) to the columns ref
|
|
77
82
|
// to prevent a SQL ambiguity error.
|
|
78
|
-
const
|
|
83
|
+
const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
|
|
84
|
+
const SELECT = query.SELECT
|
|
79
85
|
const localizedEntityName = _getLocalizedAssociation(entity).target
|
|
80
86
|
const elements = entity.elements
|
|
81
87
|
const entityName = entity.name
|
|
82
|
-
const
|
|
83
|
-
const columnRef = column.ref
|
|
84
|
-
if (!columnRef) return column
|
|
88
|
+
const getEntityName = columnRef => {
|
|
85
89
|
const columnName = columnRef[0]
|
|
86
|
-
const localizedElement = elements[columnName].localized
|
|
90
|
+
const localizedElement = !!elements[columnName].localized
|
|
87
91
|
const targetEntityName = localizedElement ? localizedEntityName : entityName
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
query.SELECT.columns = query.SELECT.columns.map(_addAliasToColumn(entityName, localizedEntityName, elements))
|
|
92
|
-
const columns = columnsToBeSearched.map(_addAliasToColumn(entityName, localizedEntityName, elements))
|
|
93
|
-
|
|
94
|
-
if (query.SELECT.groupBy) {
|
|
95
|
-
query.SELECT.groupBy = query.SELECT.groupBy.map(_addAliasToColumn(entityName, localizedEntityName, elements))
|
|
92
|
+
return targetEntityName
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
|
|
95
|
+
SELECT.columns = addAliasToExpression(SELECT.columns, getEntityName)
|
|
96
|
+
columnsToBeSearched = addAliasToExpression(columnsToBeSearched, getEntityName)
|
|
97
|
+
SELECT.groupBy = addAliasToExpression(SELECT.groupBy, getEntityName)
|
|
98
|
+
SELECT.where = addAliasToExpression(SELECT.where, getEntityName)
|
|
99
|
+
return columnsToBeSearched
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
module.exports = search2cqn4sql
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* @returns {import("../types/api").searchContainsExp} The `CONTAINS` CQN expression
|
|
26
26
|
*/
|
|
27
27
|
const searchToContains = (cqnSearchPhrase, columns) => {
|
|
28
|
-
// serialize CQN search phrase
|
|
28
|
+
// serialize the CQN search phrase
|
|
29
29
|
const searchString = cqnSearchPhrase.reduce((searchStringAccumulator, currentValue) => {
|
|
30
30
|
// Multiple search terms separated by an space are automatically
|
|
31
31
|
// interpreted as an AND operator. Therefore, it is not mandatory
|
|
@@ -58,8 +58,10 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
58
58
|
await super.emit(msg)
|
|
59
59
|
done()
|
|
60
60
|
} catch (e) {
|
|
61
|
-
failed
|
|
62
|
-
|
|
61
|
+
// In case of AMQP and Solace, the `failed` callback must be called
|
|
62
|
+
// with an error, otherwise there are problems with the redelivery count.
|
|
63
|
+
failed(new Error('processing failed'))
|
|
64
|
+
LOG.error('ERROR occured in asynchronous event processing:', e)
|
|
63
65
|
}
|
|
64
66
|
})
|
|
65
67
|
}
|
|
@@ -20,6 +20,7 @@ const _checkAppURL = appURL => {
|
|
|
20
20
|
throw new Error(
|
|
21
21
|
'Enterprise Messaging: You need to provide an HTTPS endpoint to your application.\n\nHint: You can set the application URI in environment variable `VCAP_APPLICATION.application_uris[0]`. This is needed because incoming messages are delivered through HTTP via webhooks.\nExample: `{ ..., "VCAP_APPLICATION": { "application_uris": ["my-app.com"] } }`\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-amqp`.'
|
|
22
22
|
)
|
|
23
|
+
|
|
23
24
|
if (appURL.startsWith('https://localhost'))
|
|
24
25
|
throw new Error(
|
|
25
26
|
'The endpoint of your application is local and cannot be reached from Enterprise Messaging.\n\nHint: For local development you can set up a tunnel to your local endpoint and enter its public https endpoint in `VCAP_APPLICATION.application_uris[0]`.\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-amqp`.'
|
|
@@ -53,7 +53,9 @@ class FileBasedMessaging extends MessagingService {
|
|
|
53
53
|
if (this.subscribedTopics.has(topic)) {
|
|
54
54
|
const event = this.subscribedTopics.get(topic)
|
|
55
55
|
if (!event) return
|
|
56
|
-
super
|
|
56
|
+
super
|
|
57
|
+
.emit({ event, ...json, inbound: true })
|
|
58
|
+
.catch(e => LOG.error('ERROR occured in asynchronous event processing:', e))
|
|
57
59
|
} else other.push(each + '\n')
|
|
58
60
|
}
|
|
59
61
|
} catch (e) {
|
|
@@ -26,17 +26,24 @@ class MessagingService extends OutboxService {
|
|
|
26
26
|
this.subscribedTopics = new Map()
|
|
27
27
|
// Only for one central `messaging` service, otherwise all technical services would register themselves
|
|
28
28
|
if (this.name === 'messaging') {
|
|
29
|
+
this._registeredServices = new Map()
|
|
29
30
|
// listen for all subscriptions to declared events of remote, i.e. connected services
|
|
30
31
|
cds.on('subscribe', (srv, event) => {
|
|
31
32
|
const declared = srv.events[event]
|
|
32
33
|
if (declared && srv.name in cds.requires && !srv.mocked) {
|
|
33
34
|
// we register self-handlers for declared events, which are supposed
|
|
34
35
|
// to be calles by subclasses calling this.dispatch on incoming events
|
|
36
|
+
let registeredEvents = this._registeredServices.get(srv.name)
|
|
37
|
+
if (!registeredEvents) {
|
|
38
|
+
registeredEvents = new Set()
|
|
39
|
+
this._registeredServices.set(srv.name, registeredEvents)
|
|
40
|
+
}
|
|
41
|
+
if (registeredEvents.has(event)) return
|
|
42
|
+
registeredEvents.add(event)
|
|
35
43
|
const topic = _topic(declared)
|
|
36
|
-
this.on(topic,
|
|
44
|
+
this.on(topic, msg => {
|
|
37
45
|
const { data, headers } = msg
|
|
38
|
-
|
|
39
|
-
return next()
|
|
46
|
+
return srv.tx(msg).emit({ event, data, headers, __proto__: msg })
|
|
40
47
|
})
|
|
41
48
|
}
|
|
42
49
|
})
|
|
@@ -48,10 +55,9 @@ class MessagingService extends OutboxService {
|
|
|
48
55
|
// calls to srv.emit are forwarded to this.emit, which is expected to
|
|
49
56
|
// be overwritten by subclasses to write events to message channel
|
|
50
57
|
const topic = _topic(declared)
|
|
51
|
-
srv.on(event,
|
|
58
|
+
srv.on(event, msg => {
|
|
52
59
|
const { data, headers } = msg
|
|
53
|
-
|
|
54
|
-
return next()
|
|
60
|
+
return this.tx(msg).emit({ event: topic, data, headers })
|
|
55
61
|
})
|
|
56
62
|
}
|
|
57
63
|
})
|
|
@@ -69,7 +75,8 @@ class MessagingService extends OutboxService {
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
emit(event, data, headers) {
|
|
72
|
-
const
|
|
78
|
+
const _msg = typeof event === 'object' ? event : { event, data, headers }
|
|
79
|
+
const msg = _msg instanceof cds.Event ? _msg : new cds.Event(this.message4(_msg))
|
|
73
80
|
return super.emit(msg)
|
|
74
81
|
}
|
|
75
82
|
|
|
@@ -102,6 +109,8 @@ class MessagingService extends OutboxService {
|
|
|
102
109
|
|
|
103
110
|
message4(msg) {
|
|
104
111
|
const _msg = { ...msg }
|
|
112
|
+
if (msg.inbound && !cds.context)
|
|
113
|
+
_msg.user = msg.tenant ? new cds.User.Privileged({ tenant: msg.tenant }) : new cds.User.Privileged()
|
|
105
114
|
_msg.event = _warnAndStripTopicPrefix(_msg.event)
|
|
106
115
|
if (!_msg.headers) _msg.headers = {}
|
|
107
116
|
if (!_msg.inbound) {
|