@sap/cds 5.7.5 → 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 +72 -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/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 -32
- 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/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 +2 -5
- 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 +1 -4
- 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 +60 -18
- 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/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 +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 +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/vcap.js +27 -10
- package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
- package/libx/_runtime/db/expand/expand-v2.js +21 -12
- package/libx/_runtime/db/expand/expandCQNToJoin.js +8 -6
- 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 +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/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 +19 -16
- 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/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 +1 -1
- 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 +4 -1
- package/libx/_runtime/remote/utils/client.js +33 -20
- package/libx/_runtime/remote/utils/data.js +52 -11
- package/libx/_runtime/sqlite/Service.js +1 -1
- package/libx/_runtime/sqlite/conversion.js +10 -0
- 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 +49 -21
- 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
|
@@ -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
|
|
@@ -61,7 +61,7 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
61
61
|
// In case of AMQP and Solace, the `failed` callback must be called
|
|
62
62
|
// with an error, otherwise there are problems with the redelivery count.
|
|
63
63
|
failed(new Error('processing failed'))
|
|
64
|
-
LOG.
|
|
64
|
+
LOG.error('ERROR occured in asynchronous event processing:', e)
|
|
65
65
|
}
|
|
66
66
|
})
|
|
67
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) {
|
|
@@ -75,7 +75,8 @@ class MessagingService extends OutboxService {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
emit(event, data, headers) {
|
|
78
|
-
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))
|
|
79
80
|
return super.emit(msg)
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -108,6 +109,8 @@ class MessagingService extends OutboxService {
|
|
|
108
109
|
|
|
109
110
|
message4(msg) {
|
|
110
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()
|
|
111
114
|
_msg.event = _warnAndStripTopicPrefix(_msg.event)
|
|
112
115
|
if (!_msg.headers) _msg.headers = {}
|
|
113
116
|
if (!_msg.inbound) {
|
|
@@ -5,7 +5,7 @@ const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.san
|
|
|
5
5
|
|
|
6
6
|
const cdsLocale = require('../../../../lib/req/locale')
|
|
7
7
|
|
|
8
|
-
const { convertV2ResponseData, deepSanitize } = require('./data')
|
|
8
|
+
const { convertV2ResponseData, deepSanitize, convertV2PayloadData } = require('./data')
|
|
9
9
|
|
|
10
10
|
let _cloudSdkCore
|
|
11
11
|
|
|
@@ -30,6 +30,7 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
30
30
|
const destinationName = typeof destination === 'string' && destination
|
|
31
31
|
if (destinationName) {
|
|
32
32
|
destination = await getDestination(destinationName, resolveDestinationOptions(destinationOptions, jwt))
|
|
33
|
+
if (!destination) throw new Error(`Cannot resolve destination "${destinationName}"`)
|
|
33
34
|
} else if (destination.forwardAuthToken) {
|
|
34
35
|
destination = {
|
|
35
36
|
...destination,
|
|
@@ -166,10 +167,10 @@ const _purgeODataV2 = (data, target, reqHeaders) => {
|
|
|
166
167
|
if (typeof data !== 'object' || !data.d) return data
|
|
167
168
|
|
|
168
169
|
data = data.d
|
|
169
|
-
const
|
|
170
|
-
const
|
|
170
|
+
const ieee754Compatible = reqHeaders.accept && reqHeaders.accept.includes('IEEE754Compatible=true')
|
|
171
|
+
const exponentialDecimals = ieee754Compatible && reqHeaders.accept.includes('ExponentialDecimals=true')
|
|
171
172
|
const purgedResponse = data.results || data
|
|
172
|
-
const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible)
|
|
173
|
+
const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible, exponentialDecimals)
|
|
173
174
|
return _normalizeMetadata(/^__/, data, convertedResponse)
|
|
174
175
|
}
|
|
175
176
|
|
|
@@ -321,32 +322,37 @@ const getJwt = req => {
|
|
|
321
322
|
return null
|
|
322
323
|
}
|
|
323
324
|
|
|
324
|
-
const _cqnToReqOptions = (query, kind, model) => {
|
|
325
|
+
const _cqnToReqOptions = (query, kind, model, target) => {
|
|
325
326
|
const queryObject = cds.odata.urlify(query, { kind, model })
|
|
326
|
-
|
|
327
|
+
const reqOptions = {
|
|
327
328
|
method: queryObject.method,
|
|
328
329
|
url: encodeURI(
|
|
329
330
|
queryObject.path
|
|
330
331
|
// ugly workaround for Okra not allowing spaces in ( x eq 1 )
|
|
331
332
|
.replace(/\( /g, '(')
|
|
332
333
|
.replace(/ \)/g, ')')
|
|
333
|
-
)
|
|
334
|
-
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
|
|
337
|
+
reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, target) : queryObject.body
|
|
335
338
|
}
|
|
339
|
+
return reqOptions
|
|
336
340
|
}
|
|
337
341
|
|
|
338
|
-
const _stringToReqOptions = (query, data) => {
|
|
342
|
+
const _stringToReqOptions = (query, data, target) => {
|
|
339
343
|
const cleanQuery = query.trim()
|
|
340
344
|
const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
|
|
341
345
|
const reqOptions = {
|
|
342
346
|
method: cleanQuery.substring(0, blankIndex).toUpperCase(),
|
|
343
347
|
url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
|
|
344
348
|
}
|
|
345
|
-
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD')
|
|
349
|
+
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
350
|
+
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
351
|
+
}
|
|
346
352
|
return reqOptions
|
|
347
353
|
}
|
|
348
354
|
|
|
349
|
-
const _pathToReqOptions = (method, path, data) => {
|
|
355
|
+
const _pathToReqOptions = (method, path, data, target) => {
|
|
350
356
|
let url = path
|
|
351
357
|
if (!url.startsWith('/')) {
|
|
352
358
|
// extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
|
|
@@ -358,7 +364,9 @@ const _pathToReqOptions = (method, path, data) => {
|
|
|
358
364
|
url = url.replace(/^\/\//, '/')
|
|
359
365
|
}
|
|
360
366
|
const reqOptions = { method, url }
|
|
361
|
-
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD')
|
|
367
|
+
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
368
|
+
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
369
|
+
}
|
|
362
370
|
return reqOptions
|
|
363
371
|
}
|
|
364
372
|
|
|
@@ -367,13 +375,14 @@ const _hasHeader = (headers, header) =>
|
|
|
367
375
|
.map(k => k.toLowerCase())
|
|
368
376
|
.includes(header)
|
|
369
377
|
|
|
378
|
+
// eslint-disable-next-line complexity
|
|
370
379
|
const getReqOptions = (req, query, service) => {
|
|
371
380
|
const reqOptions =
|
|
372
381
|
typeof query === 'object'
|
|
373
|
-
? _cqnToReqOptions(query, service.kind, service.model)
|
|
382
|
+
? _cqnToReqOptions(query, service.kind, service.model, req.target)
|
|
374
383
|
: typeof query === 'string'
|
|
375
|
-
? _stringToReqOptions(query, req.data)
|
|
376
|
-
: _pathToReqOptions(req.method, req.path, req.data)
|
|
384
|
+
? _stringToReqOptions(query, req.data, req.target)
|
|
385
|
+
: _pathToReqOptions(req.method, req.path, req.data, req.target)
|
|
377
386
|
|
|
378
387
|
reqOptions.headers = { accept: 'application/json,text/plain' }
|
|
379
388
|
reqOptions.timeout = service.requestTimeout
|
|
@@ -387,6 +396,12 @@ const getReqOptions = (req, query, service) => {
|
|
|
387
396
|
if (locale) reqOptions.headers['accept-language'] = locale
|
|
388
397
|
}
|
|
389
398
|
|
|
399
|
+
// forward all dwc-* headers
|
|
400
|
+
if (service.options.forward_dwc_headers) {
|
|
401
|
+
const originalHeaders = (req.context && req.context._ && req.context._.req && req.context._.req.headers) || {}
|
|
402
|
+
for (const k in originalHeaders) if (k.match(/^dwc-/)) reqOptions.headers[k] = originalHeaders[k]
|
|
403
|
+
}
|
|
404
|
+
|
|
390
405
|
if (reqOptions.data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
391
406
|
reqOptions.headers['content-type'] = 'application/json'
|
|
392
407
|
reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
|
|
@@ -394,11 +409,9 @@ const getReqOptions = (req, query, service) => {
|
|
|
394
409
|
reqOptions.url = formatPath(reqOptions.url)
|
|
395
410
|
|
|
396
411
|
// batch envelope if needed
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
reqOptions.url.length > ((cds.env.remote && cds.env.remote.max_get_url_length) || 1028)
|
|
401
|
-
) {
|
|
412
|
+
const maxGetUrlLength =
|
|
413
|
+
service.options.max_get_url_length || (cds.env.remote && cds.env.remote.max_get_url_length) || 1028
|
|
414
|
+
if (KINDS_SUPPORTING_BATCH[service.kind] && reqOptions.method === 'GET' && reqOptions.url.length > maxGetUrlLength) {
|
|
402
415
|
reqOptions._autoBatch = true
|
|
403
416
|
reqOptions.data = [
|
|
404
417
|
'--batch1',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { big } = require('@sap/cds-foss')
|
|
2
|
+
|
|
1
3
|
// Code adopted from @sap/cds-odata-v2-adapter-proxy
|
|
2
4
|
// https://www.w3.org/TR/xmlschema11-2/#nt-duDTFrag
|
|
3
5
|
const DurationRegex = /^P(?:(\d)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?T(?:(\d{1,2})H)?(?:(\d{2})M)?(?:(\d{2}(?:\.\d+)?)S)?$/i
|
|
@@ -20,15 +22,16 @@ const DataTypeOData = {
|
|
|
20
22
|
Time: 'cds.TimeOfDay'
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
const _convertData = (data, target,
|
|
25
|
+
const _convertData = (data, target, convertValueFn) => {
|
|
26
|
+
const _convertRecordFn = _getConvertRecordFn(target, convertValueFn)
|
|
24
27
|
if (Array.isArray(data)) {
|
|
25
|
-
return data.map(
|
|
28
|
+
return data.map(_convertRecordFn)
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
return
|
|
31
|
+
return _convertRecordFn(data)
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
const _getConvertRecordFn = (target,
|
|
34
|
+
const _getConvertRecordFn = (target, convertValueFn) => record => {
|
|
32
35
|
for (const key in record) {
|
|
33
36
|
if (key === '__metadata') continue
|
|
34
37
|
|
|
@@ -36,24 +39,26 @@ const _getConvertRecordFn = (target, ieee754Compatible) => record => {
|
|
|
36
39
|
if (!element) continue
|
|
37
40
|
|
|
38
41
|
const recordValue = record[key]
|
|
39
|
-
const type = _elementType(element)
|
|
40
42
|
const value = (recordValue && recordValue.results) || recordValue
|
|
41
43
|
|
|
42
44
|
if (value && (element.isAssociation || Array.isArray(value))) {
|
|
43
|
-
record[key] = _convertData(value, element._target,
|
|
45
|
+
record[key] = _convertData(value, element._target, convertValueFn)
|
|
44
46
|
} else {
|
|
45
|
-
record[key] =
|
|
47
|
+
record[key] = convertValueFn(value, element)
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
return record
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
// eslint-disable-next-line complexity
|
|
55
|
+
const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
|
|
53
56
|
if (value == null) {
|
|
54
57
|
return value
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
const type = _elementType(element)
|
|
61
|
+
|
|
57
62
|
if (['cds.Boolean'].includes(type)) {
|
|
58
63
|
if (value === 'true') {
|
|
59
64
|
value = true
|
|
@@ -63,7 +68,14 @@ const _convertValue = (value, type, ieee754Compatible) => {
|
|
|
63
68
|
} else if (['cds.Integer'].includes(type)) {
|
|
64
69
|
value = parseInt(value, 10)
|
|
65
70
|
} else if (['cds.Decimal', 'cds.Integer64', 'cds.DecimalFloat'].includes(type)) {
|
|
66
|
-
|
|
71
|
+
const bigValue = big(value)
|
|
72
|
+
if (ieee754Compatible) {
|
|
73
|
+
// TODO test with arrayed => element.items.scale?
|
|
74
|
+
value = exponentialDecimals ? bigValue.toExponential(element.scale) : bigValue.toFixed(element.scale)
|
|
75
|
+
} else {
|
|
76
|
+
// OData V2 does not even mention ieee754Compatible, but V4 requires JSON number if ieee754Compatible=false
|
|
77
|
+
value = bigValue.toNumber()
|
|
78
|
+
}
|
|
67
79
|
} else if (['cds.Double'].includes(type)) {
|
|
68
80
|
value = parseFloat(value)
|
|
69
81
|
} else if (['cds.Time'].includes(type)) {
|
|
@@ -90,6 +102,29 @@ const _convertValue = (value, type, ieee754Compatible) => {
|
|
|
90
102
|
return value
|
|
91
103
|
}
|
|
92
104
|
|
|
105
|
+
const _convertPayloadValue = (value, element) => {
|
|
106
|
+
const type = _elementType(element)
|
|
107
|
+
|
|
108
|
+
// see https://www.odata.org/documentation/odata-version-2-0/json-format/
|
|
109
|
+
if (value == null) return value
|
|
110
|
+
switch (type) {
|
|
111
|
+
case 'cds.Date':
|
|
112
|
+
case 'cds.DateTime':
|
|
113
|
+
return `/Date(${new Date(value).getTime()})/`
|
|
114
|
+
case 'cds.Binary':
|
|
115
|
+
case 'cds.LargeBinary':
|
|
116
|
+
return Buffer.isBuffer(value) ? value.toString('base64') : value
|
|
117
|
+
case 'cds.Timestamp':
|
|
118
|
+
// According to OData V2 spec, and as cds.DateTime => (V2) Edm.DateTimeOffset => cds.Timestamp,
|
|
119
|
+
// cds.Timestamp should be converted into Edm.DateTimeOffset literal form `datetimeoffset'${new Date(value).toISOString()}'`
|
|
120
|
+
// However, odata-v2-proxy forwards it literaly as `datetimeoffset'...'` which is rejected by okra.
|
|
121
|
+
// Note that OData V2 spec example also does not contain 'datetimeoffset' predicate.
|
|
122
|
+
return new Date(value).toISOString()
|
|
123
|
+
default:
|
|
124
|
+
return value
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
93
128
|
const _calculateTicksOffsetSum = text => {
|
|
94
129
|
return (text.replace(/\s/g, '').match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => {
|
|
95
130
|
return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000) // ticks are milliseconds (0), offset are minutes (1)
|
|
@@ -115,9 +150,14 @@ const _elementType = element => {
|
|
|
115
150
|
return type
|
|
116
151
|
}
|
|
117
152
|
|
|
118
|
-
const convertV2ResponseData = (data, target, ieee754Compatible) => {
|
|
153
|
+
const convertV2ResponseData = (data, target, ieee754Compatible, exponentialDecimals) => {
|
|
154
|
+
if (!target || !target.elements) return data
|
|
155
|
+
return _convertData(data, target, _convertValue(ieee754Compatible, exponentialDecimals))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const convertV2PayloadData = (data, target) => {
|
|
119
159
|
if (!target || !target.elements) return data
|
|
120
|
-
return _convertData(data, target,
|
|
160
|
+
return _convertData(data, target, _convertPayloadValue)
|
|
121
161
|
}
|
|
122
162
|
|
|
123
163
|
const deepSanitize = arg => {
|
|
@@ -132,5 +172,6 @@ const deepSanitize = arg => {
|
|
|
132
172
|
|
|
133
173
|
module.exports = {
|
|
134
174
|
convertV2ResponseData,
|
|
175
|
+
convertV2PayloadData,
|
|
135
176
|
deepSanitize
|
|
136
177
|
}
|
|
@@ -39,7 +39,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
39
39
|
this._insert = this._queries.insert(execute.insert)
|
|
40
40
|
this._read = this._queries.read(execute.select, execute.stream)
|
|
41
41
|
this._update = this._queries.update(execute.update, execute.select)
|
|
42
|
-
this._delete = this._queries.delete(execute.delete)
|
|
42
|
+
this._delete = this._queries.delete(execute.delete, execute.update)
|
|
43
43
|
this._run = this._queries.run(this._insert, this._read, this._update, this._delete, execute.cqn, execute.sql)
|
|
44
44
|
|
|
45
45
|
this.dbcs = new Map()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
2
|
+
|
|
1
3
|
const convertToBoolean = boolean => {
|
|
2
4
|
if (boolean === null) return null
|
|
3
5
|
|
|
@@ -41,4 +43,12 @@ const SQLITE_TYPE_CONVERSION_MAP = new Map([
|
|
|
41
43
|
['cds.Timestamp', convertToISOTime]
|
|
42
44
|
])
|
|
43
45
|
|
|
46
|
+
if (cds.env.features.bigjs) {
|
|
47
|
+
const Big = require('big.js')
|
|
48
|
+
const convertToBig = value => new Big(value)
|
|
49
|
+
|
|
50
|
+
SQLITE_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
|
|
51
|
+
SQLITE_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
module.exports = { SQLITE_TYPE_CONVERSION_MAP }
|
|
@@ -94,14 +94,14 @@
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* @typedef {object} search2cqnOptions
|
|
97
|
-
* @property {ColumnRefs} [columns] The columns to
|
|
98
|
-
* be searched
|
|
97
|
+
* @property {ColumnRefs} [columns] The columns to be searched
|
|
99
98
|
* @property {string} locale The user locale
|
|
100
99
|
*/
|
|
101
100
|
|
|
102
101
|
/**
|
|
103
102
|
* @typedef {object} cqn2cqn4sqlOptions
|
|
104
103
|
* @property {boolean} [suppressSearch=false] Indicates whether the search handler is called.
|
|
104
|
+
* @property {boolean} [_4fiori]
|
|
105
105
|
* @property {import('../db/Service')} [service]
|
|
106
106
|
*/
|
|
107
107
|
|
|
@@ -45,6 +45,7 @@ const traverseField = (info, field) => {
|
|
|
45
45
|
const traverseFieldNodes = (info, fieldNodes) => fieldNodes.map(fieldNode => traverseField(info, fieldNode))
|
|
46
46
|
|
|
47
47
|
module.exports = info => {
|
|
48
|
+
// REVISIT: JSON.parse(JSON.stringify(obj)) breaks buffers
|
|
48
49
|
const deepClonedFieldNodes = JSON.parse(JSON.stringify(info.fieldNodes))
|
|
49
50
|
traverseFieldNodes(info, deepClonedFieldNodes)
|
|
50
51
|
return deepClonedFieldNodes
|
|
@@ -3,12 +3,23 @@ const cds = require('../_runtime/cds')
|
|
|
3
3
|
const { where2obj } = require('../_runtime/common/utils/cqn')
|
|
4
4
|
const { findCsnTargetFor } = require('../_runtime/common/utils/csn')
|
|
5
5
|
|
|
6
|
+
const _addKeysDeep = (keys, keysCollector) => {
|
|
7
|
+
for (const keyName in keys) {
|
|
8
|
+
const key = keys[keyName]
|
|
9
|
+
if (key.type === 'cds.Association' || key['@odata.foreignKey4'] === 'up_') continue
|
|
10
|
+
if ('elements' in key) {
|
|
11
|
+
_addKeysDeep(key.elements, keysCollector)
|
|
12
|
+
continue
|
|
13
|
+
}
|
|
14
|
+
keysCollector.push(keyName)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
function _keysOf(entity) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
: []
|
|
19
|
+
if (!entity || !entity.keys) return
|
|
20
|
+
const keysCollector = []
|
|
21
|
+
_addKeysDeep(entity.keys, keysCollector)
|
|
22
|
+
return keysCollector
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
function _getDefinition(definition, name, namespace) {
|
|
@@ -49,7 +60,7 @@ function _processSegments(cqn, model, namespace) {
|
|
|
49
60
|
let one
|
|
50
61
|
for (let i = 0; i < ref.length; i++) {
|
|
51
62
|
const seg = ref[i].id || ref[i]
|
|
52
|
-
|
|
63
|
+
let params = ref[i].where && where2obj(ref[i].where)
|
|
53
64
|
|
|
54
65
|
if (incompleteKeys) {
|
|
55
66
|
// > key
|
|
@@ -91,6 +102,9 @@ function _processSegments(cqn, model, namespace) {
|
|
|
91
102
|
if (ref[i].where) {
|
|
92
103
|
keyCount += addRefToWhereIfNecessary(ref[i].where, current)
|
|
93
104
|
_resolveAliasInWhere(ref[i].where, current)
|
|
105
|
+
// in case of Foo(1), params will be {} (before addRefToWhereIfNecessary was called)
|
|
106
|
+
if (!Object.keys(params).length) params = where2obj(ref[i].where)
|
|
107
|
+
_checkAllKeysProvided(params, current)
|
|
94
108
|
}
|
|
95
109
|
} else if ({ action: 1, function: 1 }[current.kind]) {
|
|
96
110
|
// > action or function
|
|
@@ -146,6 +160,15 @@ function _processSegments(cqn, model, namespace) {
|
|
|
146
160
|
|
|
147
161
|
const _resolveFrom = from => (from.SELECT ? _resolveFrom(from.SELECT.from) : from)
|
|
148
162
|
|
|
163
|
+
const _checkAllKeysProvided = (params, entity) => {
|
|
164
|
+
const keysOfEntity = _keysOf(entity)
|
|
165
|
+
if (!keysOfEntity) return
|
|
166
|
+
for (const keyOfEntity of keysOfEntity) {
|
|
167
|
+
if (!(keyOfEntity in params))
|
|
168
|
+
throw Object.assign(new Error(`Key "${keyOfEntity}" is missing for entity "${entity.name}"`), { status: 400 })
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
149
172
|
function _4service(service) {
|
|
150
173
|
const { namespace, model } = service
|
|
151
174
|
|
package/libx/odata/cqn2odata.js
CHANGED
|
@@ -357,6 +357,15 @@ function $orderBy(orderBy) {
|
|
|
357
357
|
if (hasValidProps(cur, 'ref')) {
|
|
358
358
|
res.push(cur.ref.join('/'))
|
|
359
359
|
}
|
|
360
|
+
|
|
361
|
+
if (hasValidProps(cur, 'func', 'sort')) {
|
|
362
|
+
res.push(`${cur.func}(${_args(cur.args)})` + ' ' + cur.sort)
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (hasValidProps(cur, 'func')) {
|
|
367
|
+
res.push(`${cur.func}(${_args(cur.args)})`)
|
|
368
|
+
}
|
|
360
369
|
}
|
|
361
370
|
|
|
362
371
|
return '$orderby=' + res.join(',')
|