@sap/cds 5.7.3 → 5.8.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 +111 -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 -38
- 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 +13 -7
- 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 +1 -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 +17 -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 +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
- 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 +47 -10
- 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 +3 -3
- package/libx/_runtime/common/composition/insert.js +14 -27
- package/libx/_runtime/common/composition/update.js +39 -34
- package/libx/_runtime/common/error/frontend.js +19 -5
- 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 +2 -1
- package/libx/_runtime/common/utils/cqn.js +2 -6
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
- package/libx/_runtime/common/utils/csn.js +15 -4
- 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 +11 -5
- 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 +57 -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 -4
- package/libx/_runtime/db/query/read.js +4 -1
- 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/SelectBuilder.js +7 -3
- 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 -20
- package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
- package/libx/_runtime/fiori/utils/handler.js +1 -11
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/conversion.js +12 -1
- package/libx/_runtime/hana/execute.js +31 -16
- package/libx/_runtime/hana/localized.js +1 -1
- package/libx/_runtime/hana/search.js +3 -3
- package/libx/_runtime/hana/search2cqn4sql.js +23 -25
- 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/service.js +16 -7
- package/libx/_runtime/remote/utils/client.js +37 -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/crud/update.js +8 -5
- 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 +50 -22
- package/libx/odata/index.js +2 -2
- 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
|
@@ -3,10 +3,8 @@ const { SELECT } = cds.ql
|
|
|
3
3
|
|
|
4
4
|
const { cqn2cqn4sql, convertWhereExists } = require('../../common/utils/cqn2cqn4sql')
|
|
5
5
|
const { getElementDeep } = require('../../common/utils/csn')
|
|
6
|
-
|
|
7
6
|
const { DRAFT_COLUMNS, DRAFT_COLUMNS_MAP, SCENARIO } = require('../../common/constants/draft')
|
|
8
7
|
const {
|
|
9
|
-
adaptStreamCQN,
|
|
10
8
|
addColumnAlias,
|
|
11
9
|
draftIsLocked,
|
|
12
10
|
ensureDraftsSuffix,
|
|
@@ -18,8 +16,8 @@ const {
|
|
|
18
16
|
filterKeys
|
|
19
17
|
} = require('../utils/handler')
|
|
20
18
|
const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively } = require('../utils/where')
|
|
21
|
-
|
|
22
19
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
20
|
+
const { adaptStreamCQN } = require('../../cds-services/adapter/odata-v4/utils/stream')
|
|
23
21
|
|
|
24
22
|
const _findRootSubSelectFor = query => {
|
|
25
23
|
if (query.SELECT.where) {
|
|
@@ -50,7 +48,8 @@ const _getWhereWithAppendedDraftRestrictions = (where = [], req, scenarioAlias,
|
|
|
50
48
|
})
|
|
51
49
|
|
|
52
50
|
if (where.length) where.push('and')
|
|
53
|
-
|
|
51
|
+
// restriction might contain or clause -> use xpr for grouping
|
|
52
|
+
xpr.includes('or') ? where.push({ xpr }) : where.push(...xpr)
|
|
54
53
|
} else {
|
|
55
54
|
// > restriction inherited from parent via autoexposure
|
|
56
55
|
// find inner most sub select if available and append restriction to where clause
|
|
@@ -778,13 +777,16 @@ const _getDraftDoc = (req, draftName, draftWhere) => {
|
|
|
778
777
|
return draftDocs
|
|
779
778
|
}
|
|
780
779
|
|
|
781
|
-
const _getOrderByEnrichedColumns = (orderBy, columns) => {
|
|
780
|
+
const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
|
|
782
781
|
const enrichedCol = []
|
|
783
782
|
if (orderBy && orderBy.length > 1) {
|
|
784
783
|
const colNames = columns.map(el => el.ref[el.ref.length - 1])
|
|
785
784
|
// REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
|
|
786
785
|
for (const el of orderBy) {
|
|
787
|
-
|
|
786
|
+
// For associations we need to 'materialise' the resulting field, otherwise we cannot access it in an outer SELECT.
|
|
787
|
+
if (entity && entity.elements[el.ref[0]] && entity.elements[el.ref[0]].isAssociation) {
|
|
788
|
+
enrichedCol.push({ ref: [...el.ref], as: _poorMansAlias4(el) })
|
|
789
|
+
} else if (!DRAFT_COLUMNS.includes(el.ref[el.ref.length - 1]) && !colNames.includes(el.ref[el.ref.length - 1])) {
|
|
788
790
|
enrichedCol.push({ ref: [...el.ref] })
|
|
789
791
|
}
|
|
790
792
|
}
|
|
@@ -806,11 +808,11 @@ const _replaceDraftAlias = where => {
|
|
|
806
808
|
|
|
807
809
|
const _poorMansAlias4 = xpr => '_' + xpr.ref.join('_') + '_'
|
|
808
810
|
|
|
809
|
-
const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) => {
|
|
811
|
+
const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model, entity) => {
|
|
810
812
|
const draftActiveWhere = _getWhereForActive(draftWhere)
|
|
811
813
|
const activeDocs = getEnrichedCQN(SELECT.from(req.target), req.query.SELECT, draftActiveWhere, undefined, false)
|
|
812
814
|
activeDocs.where(_getWhereWithAppendedDraftRestrictions([], req))
|
|
813
|
-
convertWhereExists(activeDocs, model, {})
|
|
815
|
+
convertWhereExists(activeDocs.SELECT, model, {})
|
|
814
816
|
|
|
815
817
|
// @restrict.where not applicable for drafts (I can ALWAYS read mine)
|
|
816
818
|
_replaceDraftAlias(draftWhere)
|
|
@@ -837,7 +839,7 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) =>
|
|
|
837
839
|
return union.columns({ func: 'sum', args: [{ ref: ['$count'] }], as: '$count' })
|
|
838
840
|
}
|
|
839
841
|
|
|
840
|
-
const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns)
|
|
842
|
+
const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns, entity)
|
|
841
843
|
|
|
842
844
|
for (const col of enrichedColumns) {
|
|
843
845
|
// if we have columns for outer order by that may also be needed for joins, we need to duplicate them
|
|
@@ -914,12 +916,14 @@ const _excludeActiveDraftExists = (req, draftWhere, columns, model) => {
|
|
|
914
916
|
])
|
|
915
917
|
.where(_inProcessByUserWhere(req.user.id))
|
|
916
918
|
|
|
919
|
+
const targetName = ensureNoDraftsSuffix(req.target.name)
|
|
917
920
|
for (const key of _getTargetKeys(req)) {
|
|
918
|
-
subSelect.where([{ ref: [
|
|
921
|
+
subSelect.where([{ ref: [targetName, key] }, '=', { ref: [draftName, key] }])
|
|
919
922
|
}
|
|
920
923
|
|
|
924
|
+
const entity = model.definitions[targetName]
|
|
921
925
|
draftWhere = removeIsActiveEntityRecursively(draftWhere)
|
|
922
|
-
const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model)
|
|
926
|
+
const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model, entity)
|
|
923
927
|
cqn.SELECT.from.as = name
|
|
924
928
|
|
|
925
929
|
if (cqn.SELECT.orderBy) {
|
|
@@ -959,13 +963,16 @@ const _validatedWithSiblingInProcess = (req, draftWhere, draftParameters, column
|
|
|
959
963
|
)
|
|
960
964
|
return _excludeActiveDraftExists(req, draftWhere, columns, model)
|
|
961
965
|
if (
|
|
966
|
+
draftInProcessByUser &&
|
|
962
967
|
draftInProcessByUser.op === '!=' &&
|
|
963
968
|
_isValidWithDraftLocked(isActiveEntity, siblingIsActive, draftInProcessByUser)
|
|
964
969
|
) {
|
|
965
970
|
return _activeWithDraftInProcess(req, draftWhere, columns, req.user.id)
|
|
966
|
-
} else if (_isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
|
|
971
|
+
} else if (draftInProcessByUser && _isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
|
|
967
972
|
return _activeWithDraftInProcess(req, draftWhere, columns, null)
|
|
968
973
|
}
|
|
974
|
+
|
|
975
|
+
//
|
|
969
976
|
}
|
|
970
977
|
|
|
971
978
|
const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, columns) =>
|
|
@@ -1214,16 +1221,13 @@ const _handler = async function (req) {
|
|
|
1214
1221
|
// handle localized here as it was previously handled for req.target
|
|
1215
1222
|
req.target = _getLocalizedEntity(this.model, req.target, req.user) || req.target
|
|
1216
1223
|
|
|
1217
|
-
// REVISIT
|
|
1218
|
-
delete req.query._validationQuery
|
|
1219
|
-
|
|
1220
1224
|
const originalFrom = _copyCQNPartial(req.query.SELECT.from)
|
|
1221
1225
|
|
|
1222
1226
|
// REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
|
|
1223
|
-
const
|
|
1227
|
+
const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
|
|
1224
1228
|
|
|
1225
1229
|
// do not clone with Object.assign as that would skip all non-enumerable properties
|
|
1226
|
-
const reqClone = { __proto__: req, query: _copyCQNPartial(
|
|
1230
|
+
const reqClone = { __proto__: req, query: _copyCQNPartial(query4sql) }
|
|
1227
1231
|
|
|
1228
1232
|
// ensure draft restrictions are copied to new query
|
|
1229
1233
|
reqClone.query._draftRestrictions = req.query._draftRestrictions
|
|
@@ -1233,6 +1237,7 @@ const _handler = async function (req) {
|
|
|
1233
1237
|
reqClone.query._streaming = true
|
|
1234
1238
|
return cds.tx(req).run(reqClone.query)
|
|
1235
1239
|
}
|
|
1240
|
+
|
|
1236
1241
|
let cqnScenario
|
|
1237
1242
|
|
|
1238
1243
|
// to replace scenario CQNs for queries with $apply SELECT chain (new odata2cqn parser)
|
|
@@ -1263,16 +1268,13 @@ const _handler = async function (req) {
|
|
|
1263
1268
|
)
|
|
1264
1269
|
|
|
1265
1270
|
_adaptSubSelects(cqnScenario.cqn, cqnScenario.scenario)
|
|
1266
|
-
|
|
1267
1271
|
_adaptAnnotationAliases(cqnScenario.cqn)
|
|
1268
1272
|
|
|
1269
1273
|
// unlocalize for db and after handlers as it was before
|
|
1270
1274
|
req.target = this.model.definitions[ensureUnlocalized(req.target.name)]
|
|
1271
1275
|
|
|
1272
1276
|
const result = await cds.tx(req).send({ query: cqnScenario.cqn, target: req.target })
|
|
1273
|
-
|
|
1274
1277
|
const resultAsArray = Array.isArray(result) ? result : result ? [result] : []
|
|
1275
|
-
|
|
1276
1278
|
removeDraftUUIDIfNecessary(resultAsArray, req)
|
|
1277
1279
|
|
|
1278
1280
|
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
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { UPDATE, SELECT } = cds.ql
|
|
3
|
-
const { removeIsActiveEntityRecursively, isActiveEntityRequested } = require('./where')
|
|
4
3
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
5
4
|
const { ensureNoDraftsSuffix, ensureDraftsSuffix, ensureUnlocalized } = require('../../common/utils/draft')
|
|
6
5
|
const getTemplate = require('../../common/utils/template')
|
|
@@ -126,7 +125,7 @@ const getEnrichedCQN = (cqn, select, draftWhere, scenarioAlias, addLimitOrder =
|
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
if (select.distinct) {
|
|
129
|
-
cqn.distinct
|
|
128
|
+
cqn.SELECT.distinct = true
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
const alias = (select.from && select.from.as) || scenarioAlias
|
|
@@ -249,14 +248,6 @@ const replaceRefWithDraft = ref => {
|
|
|
249
248
|
ref[0] = ensureDraftsSuffix(ref[0])
|
|
250
249
|
}
|
|
251
250
|
|
|
252
|
-
const adaptStreamCQN = cqn => {
|
|
253
|
-
if (isActiveEntityRequested(cqn.SELECT.where)) {
|
|
254
|
-
cqn.SELECT.where = removeIsActiveEntityRecursively(cqn.SELECT.where)
|
|
255
|
-
} else {
|
|
256
|
-
replaceRefWithDraft(cqn.SELECT.from.ref)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
251
|
const draftIsLocked = lastChangedAt => {
|
|
261
252
|
// default timeout timer is 15 minutes
|
|
262
253
|
const DRAFT_CANCEL_TIMEOUT_IN_MS = ((cds.env.drafts && cds.env.drafts.cancellationTimeout) || 15) * 60 * 1000
|
|
@@ -289,7 +280,6 @@ module.exports = {
|
|
|
289
280
|
hasDraft,
|
|
290
281
|
proxifyToNoDraftsName,
|
|
291
282
|
addColumnAlias,
|
|
292
|
-
adaptStreamCQN,
|
|
293
283
|
replaceRefWithDraft,
|
|
294
284
|
getKeyProperty,
|
|
295
285
|
filterKeys,
|
|
@@ -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
|
|
@@ -44,9 +46,18 @@ const HANA_TYPE_CONVERSION_MAP = new Map([
|
|
|
44
46
|
['cds.Integer64', convertInt64ToString],
|
|
45
47
|
['cds.DateTime', convertToISONoMillis],
|
|
46
48
|
['cds.Timestamp', convertToISO],
|
|
47
|
-
['cds.LargeString', convertToString]
|
|
49
|
+
['cds.LargeString', convertToString],
|
|
50
|
+
['cds.hana.CLOB', convertToString]
|
|
48
51
|
])
|
|
49
52
|
|
|
53
|
+
if (cds.env.features.bigjs) {
|
|
54
|
+
const Big = require('big.js')
|
|
55
|
+
const convertToBig = value => new Big(value)
|
|
56
|
+
|
|
57
|
+
HANA_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
|
|
58
|
+
HANA_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
module.exports = {
|
|
51
62
|
HANA_TYPE_CONVERSION_MAP
|
|
52
63
|
}
|
|
@@ -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,24 @@ 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
|
+
27: 'BLOB'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _getBinaries(stmt) {
|
|
58
|
+
// hdb vs. @sap/hana-client
|
|
59
|
+
const parameters = stmt.parameterMetadata || stmt.getParameterInfo()
|
|
60
|
+
const typeKey = stmt.parameterMetadata ? 'dataType' : 'nativeType'
|
|
61
|
+
return parameters.reduce((acc, cur, i) => {
|
|
62
|
+
if (BINARY_TYPES[cur[typeKey]]) acc.push(i)
|
|
63
|
+
return acc
|
|
64
|
+
}, [])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
|
|
68
|
+
|
|
51
69
|
function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
52
70
|
dbc.prepare(sql, function (err, stmt) {
|
|
53
71
|
if (err) {
|
|
@@ -56,18 +74,15 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
|
56
74
|
return reject(err)
|
|
57
75
|
}
|
|
58
76
|
|
|
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)))
|
|
77
|
+
// convert binary strings to buffers ()
|
|
78
|
+
if (cds.env.hana.base64_to_buffer !== false && _hasValues(values)) {
|
|
79
|
+
const binaries = _getBinaries(stmt)
|
|
80
|
+
if (binaries.length) {
|
|
81
|
+
const vals = Array.isArray(values[0]) ? values : [values]
|
|
82
|
+
for (const i of binaries) {
|
|
83
|
+
for (const row of vals) {
|
|
84
|
+
if (row[i] && typeof row[i] === 'string' && row[i].match(BASE64)) {
|
|
85
|
+
row[i] = Buffer.from(row[i], 'base64')
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
}
|
|
@@ -108,7 +123,7 @@ function _executeSimpleSQL(dbc, sql, values) {
|
|
|
108
123
|
values = Object.values(values)
|
|
109
124
|
}
|
|
110
125
|
// ensure that stored procedure with parameters is always executed as prepared
|
|
111
|
-
if (_hasValues(
|
|
126
|
+
if (_hasValues(values) || sql.match(/^call.*?\?.*$/i)) {
|
|
112
127
|
_executeAsPreparedStatement(dbc, sql, values, reject, resolve)
|
|
113
128
|
} else {
|
|
114
129
|
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,56 +46,54 @@ 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
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
const _getLocalizedAssociation = entity =>
|
|
71
|
-
const associations = entity.associations
|
|
72
|
-
return associations && associations.localized
|
|
73
|
-
}
|
|
75
|
+
const _getLocalizedAssociation = entity => entity.associations && entity.associations.localized
|
|
74
76
|
|
|
75
77
|
// The inner join modifies the original SELECT ... FROM query and adds ambiguity,
|
|
76
78
|
// therefore add the table/entity name (as a preceding element) to the columns ref
|
|
77
79
|
// to prevent a SQL ambiguity error.
|
|
78
|
-
const
|
|
80
|
+
const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
|
|
81
|
+
const SELECT = query.SELECT
|
|
79
82
|
const localizedEntityName = _getLocalizedAssociation(entity).target
|
|
80
83
|
const elements = entity.elements
|
|
81
84
|
const entityName = entity.name
|
|
82
|
-
const
|
|
83
|
-
const columnRef = column.ref
|
|
84
|
-
if (!columnRef) return column
|
|
85
|
+
const getEntityName = columnRef => {
|
|
85
86
|
const columnName = columnRef[0]
|
|
86
|
-
const localizedElement = elements[columnName].localized
|
|
87
|
+
const localizedElement = !!elements[columnName].localized
|
|
87
88
|
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))
|
|
89
|
+
return targetEntityName
|
|
96
90
|
}
|
|
97
91
|
|
|
98
|
-
|
|
92
|
+
SELECT.columns = addAliasToExpression(SELECT.columns, getEntityName)
|
|
93
|
+
columnsToBeSearched = addAliasToExpression(columnsToBeSearched, getEntityName)
|
|
94
|
+
SELECT.groupBy = addAliasToExpression(SELECT.groupBy, getEntityName)
|
|
95
|
+
SELECT.where = addAliasToExpression(SELECT.where, getEntityName)
|
|
96
|
+
return columnsToBeSearched
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
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) {
|