@sap/cds 5.7.5 → 5.8.2
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 +97 -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/log/format/kibana.js +3 -3
- 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/ODataRequest.js +1 -3
- 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/ResourcePathParser.js +23 -2
- 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 +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 +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 +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/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/tree.js +1 -1
- 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 +29 -6
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +21 -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 +10 -4
- 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 +67 -26
- 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 +16 -14
- 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 +20 -19
- 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/dynatrace.js +11 -5
- package/libx/_runtime/hana/execute.js +132 -19
- 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 +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 +41 -24
- package/libx/_runtime/remote/utils/data.js +54 -12
- 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/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 +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
|
@@ -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,
|
|
@@ -141,8 +142,8 @@ function _defineProperty(obj, property, value) {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
function _normalizeMetadata(prefix, data, results) {
|
|
144
|
-
const target = results
|
|
145
|
-
if (typeof target !== 'object') return target
|
|
145
|
+
const target = results !== undefined ? results : data
|
|
146
|
+
if (typeof target !== 'object' || target === null) return target
|
|
146
147
|
const metadataKeys = Object.keys(data).filter(k => prefix.test(k))
|
|
147
148
|
for (const k of metadataKeys) {
|
|
148
149
|
const $ = k.replace(prefix, '$')
|
|
@@ -166,17 +167,17 @@ 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
|
|
171
|
-
const purgedResponse = data.results
|
|
172
|
-
const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible)
|
|
170
|
+
const ieee754Compatible = reqHeaders.accept && reqHeaders.accept.includes('IEEE754Compatible=true')
|
|
171
|
+
const exponentialDecimals = ieee754Compatible && reqHeaders.accept.includes('ExponentialDecimals=true')
|
|
172
|
+
const purgedResponse = 'results' in data ? data.results : data
|
|
173
|
+
const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible, exponentialDecimals)
|
|
173
174
|
return _normalizeMetadata(/^__/, data, convertedResponse)
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
const _purgeODataV4 = data => {
|
|
177
178
|
if (typeof data !== 'object') return data
|
|
178
179
|
|
|
179
|
-
const purgedResponse = data.value
|
|
180
|
+
const purgedResponse = 'value' in data ? data.value : data
|
|
180
181
|
return _normalizeMetadata(/^@odata\./, data, purgedResponse)
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -245,6 +246,7 @@ const run = async (
|
|
|
245
246
|
|
|
246
247
|
LOG._warn && LOG.warn(sanitizedError)
|
|
247
248
|
|
|
249
|
+
// REVISIT: switch from innererror to reason in cds^6
|
|
248
250
|
throw Object.assign(new Error(e.message), { statusCode: 502, innererror: sanitizedError })
|
|
249
251
|
}
|
|
250
252
|
|
|
@@ -266,6 +268,7 @@ const run = async (
|
|
|
266
268
|
|
|
267
269
|
LOG._warn && LOG.warn(sanitizedError)
|
|
268
270
|
|
|
271
|
+
// REVISIT: switch from innererror to reason in cds^6
|
|
269
272
|
throw Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
|
|
270
273
|
statusCode: 502,
|
|
271
274
|
innererror: sanitizedError
|
|
@@ -293,6 +296,8 @@ const run = async (
|
|
|
293
296
|
: 'Request to remote service failed.'
|
|
294
297
|
const sanitizedError = _getSanitizedError(contentJSON, requestConfig)
|
|
295
298
|
LOG._warn && LOG.warn(sanitizedError)
|
|
299
|
+
|
|
300
|
+
// REVISIT: switch from innererror to reason in cds^6
|
|
296
301
|
throw Object.assign(new Error(contentJSON.message), { statusCode: 502, innererror: sanitizedError })
|
|
297
302
|
}
|
|
298
303
|
}
|
|
@@ -321,32 +326,37 @@ const getJwt = req => {
|
|
|
321
326
|
return null
|
|
322
327
|
}
|
|
323
328
|
|
|
324
|
-
const _cqnToReqOptions = (query, kind, model) => {
|
|
329
|
+
const _cqnToReqOptions = (query, kind, model, target) => {
|
|
325
330
|
const queryObject = cds.odata.urlify(query, { kind, model })
|
|
326
|
-
|
|
331
|
+
const reqOptions = {
|
|
327
332
|
method: queryObject.method,
|
|
328
333
|
url: encodeURI(
|
|
329
334
|
queryObject.path
|
|
330
335
|
// ugly workaround for Okra not allowing spaces in ( x eq 1 )
|
|
331
336
|
.replace(/\( /g, '(')
|
|
332
337
|
.replace(/ \)/g, ')')
|
|
333
|
-
)
|
|
334
|
-
data: queryObject.body
|
|
338
|
+
)
|
|
335
339
|
}
|
|
340
|
+
if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
|
|
341
|
+
reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, target) : queryObject.body
|
|
342
|
+
}
|
|
343
|
+
return reqOptions
|
|
336
344
|
}
|
|
337
345
|
|
|
338
|
-
const _stringToReqOptions = (query, data) => {
|
|
346
|
+
const _stringToReqOptions = (query, data, target) => {
|
|
339
347
|
const cleanQuery = query.trim()
|
|
340
348
|
const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
|
|
341
349
|
const reqOptions = {
|
|
342
350
|
method: cleanQuery.substring(0, blankIndex).toUpperCase(),
|
|
343
351
|
url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
|
|
344
352
|
}
|
|
345
|
-
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD')
|
|
353
|
+
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
354
|
+
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
355
|
+
}
|
|
346
356
|
return reqOptions
|
|
347
357
|
}
|
|
348
358
|
|
|
349
|
-
const _pathToReqOptions = (method, path, data) => {
|
|
359
|
+
const _pathToReqOptions = (method, path, data, target) => {
|
|
350
360
|
let url = path
|
|
351
361
|
if (!url.startsWith('/')) {
|
|
352
362
|
// extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
|
|
@@ -358,7 +368,9 @@ const _pathToReqOptions = (method, path, data) => {
|
|
|
358
368
|
url = url.replace(/^\/\//, '/')
|
|
359
369
|
}
|
|
360
370
|
const reqOptions = { method, url }
|
|
361
|
-
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD')
|
|
371
|
+
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
372
|
+
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
373
|
+
}
|
|
362
374
|
return reqOptions
|
|
363
375
|
}
|
|
364
376
|
|
|
@@ -367,13 +379,14 @@ const _hasHeader = (headers, header) =>
|
|
|
367
379
|
.map(k => k.toLowerCase())
|
|
368
380
|
.includes(header)
|
|
369
381
|
|
|
382
|
+
// eslint-disable-next-line complexity
|
|
370
383
|
const getReqOptions = (req, query, service) => {
|
|
371
384
|
const reqOptions =
|
|
372
385
|
typeof query === 'object'
|
|
373
|
-
? _cqnToReqOptions(query, service.kind, service.model)
|
|
386
|
+
? _cqnToReqOptions(query, service.kind, service.model, req.target)
|
|
374
387
|
: typeof query === 'string'
|
|
375
|
-
? _stringToReqOptions(query, req.data)
|
|
376
|
-
: _pathToReqOptions(req.method, req.path, req.data)
|
|
388
|
+
? _stringToReqOptions(query, req.data, req.target)
|
|
389
|
+
: _pathToReqOptions(req.method, req.path, req.data, req.target)
|
|
377
390
|
|
|
378
391
|
reqOptions.headers = { accept: 'application/json,text/plain' }
|
|
379
392
|
reqOptions.timeout = service.requestTimeout
|
|
@@ -387,6 +400,12 @@ const getReqOptions = (req, query, service) => {
|
|
|
387
400
|
if (locale) reqOptions.headers['accept-language'] = locale
|
|
388
401
|
}
|
|
389
402
|
|
|
403
|
+
// forward all dwc-* headers
|
|
404
|
+
if (service.options.forward_dwc_headers) {
|
|
405
|
+
const originalHeaders = (req.context && req.context._ && req.context._.req && req.context._.req.headers) || {}
|
|
406
|
+
for (const k in originalHeaders) if (k.match(/^dwc-/)) reqOptions.headers[k] = originalHeaders[k]
|
|
407
|
+
}
|
|
408
|
+
|
|
390
409
|
if (reqOptions.data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
391
410
|
reqOptions.headers['content-type'] = 'application/json'
|
|
392
411
|
reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
|
|
@@ -394,11 +413,9 @@ const getReqOptions = (req, query, service) => {
|
|
|
394
413
|
reqOptions.url = formatPath(reqOptions.url)
|
|
395
414
|
|
|
396
415
|
// batch envelope if needed
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
reqOptions.url.length > ((cds.env.remote && cds.env.remote.max_get_url_length) || 1028)
|
|
401
|
-
) {
|
|
416
|
+
const maxGetUrlLength =
|
|
417
|
+
service.options.max_get_url_length || (cds.env.remote && cds.env.remote.max_get_url_length) || 1028
|
|
418
|
+
if (KINDS_SUPPORTING_BATCH[service.kind] && reqOptions.method === 'GET' && reqOptions.url.length > maxGetUrlLength) {
|
|
402
419
|
reqOptions._autoBatch = true
|
|
403
420
|
reqOptions.data = [
|
|
404
421
|
'--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,27 @@ const _getConvertRecordFn = (target, ieee754Compatible) => record => {
|
|
|
36
39
|
if (!element) continue
|
|
37
40
|
|
|
38
41
|
const recordValue = record[key]
|
|
39
|
-
const
|
|
40
|
-
|
|
42
|
+
const value =
|
|
43
|
+
(recordValue && typeof recordValue === 'object' && 'results' in recordValue && recordValue.results) || recordValue
|
|
41
44
|
|
|
42
45
|
if (value && (element.isAssociation || Array.isArray(value))) {
|
|
43
|
-
record[key] = _convertData(value, element._target,
|
|
46
|
+
record[key] = _convertData(value, element._target, convertValueFn)
|
|
44
47
|
} else {
|
|
45
|
-
record[key] =
|
|
48
|
+
record[key] = convertValueFn(value, element)
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
return record
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
// eslint-disable-next-line complexity
|
|
56
|
+
const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
|
|
53
57
|
if (value == null) {
|
|
54
58
|
return value
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
const type = _elementType(element)
|
|
62
|
+
|
|
57
63
|
if (['cds.Boolean'].includes(type)) {
|
|
58
64
|
if (value === 'true') {
|
|
59
65
|
value = true
|
|
@@ -63,7 +69,14 @@ const _convertValue = (value, type, ieee754Compatible) => {
|
|
|
63
69
|
} else if (['cds.Integer'].includes(type)) {
|
|
64
70
|
value = parseInt(value, 10)
|
|
65
71
|
} else if (['cds.Decimal', 'cds.Integer64', 'cds.DecimalFloat'].includes(type)) {
|
|
66
|
-
|
|
72
|
+
const bigValue = big(value)
|
|
73
|
+
if (ieee754Compatible) {
|
|
74
|
+
// TODO test with arrayed => element.items.scale?
|
|
75
|
+
value = exponentialDecimals ? bigValue.toExponential(element.scale) : bigValue.toFixed(element.scale)
|
|
76
|
+
} else {
|
|
77
|
+
// OData V2 does not even mention ieee754Compatible, but V4 requires JSON number if ieee754Compatible=false
|
|
78
|
+
value = bigValue.toNumber()
|
|
79
|
+
}
|
|
67
80
|
} else if (['cds.Double'].includes(type)) {
|
|
68
81
|
value = parseFloat(value)
|
|
69
82
|
} else if (['cds.Time'].includes(type)) {
|
|
@@ -90,6 +103,29 @@ const _convertValue = (value, type, ieee754Compatible) => {
|
|
|
90
103
|
return value
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
const _convertPayloadValue = (value, element) => {
|
|
107
|
+
const type = _elementType(element)
|
|
108
|
+
|
|
109
|
+
// see https://www.odata.org/documentation/odata-version-2-0/json-format/
|
|
110
|
+
if (value == null) return value
|
|
111
|
+
switch (type) {
|
|
112
|
+
case 'cds.Date':
|
|
113
|
+
case 'cds.DateTime':
|
|
114
|
+
return `/Date(${new Date(value).getTime()})/`
|
|
115
|
+
case 'cds.Binary':
|
|
116
|
+
case 'cds.LargeBinary':
|
|
117
|
+
return Buffer.isBuffer(value) ? value.toString('base64') : value
|
|
118
|
+
case 'cds.Timestamp':
|
|
119
|
+
// According to OData V2 spec, and as cds.DateTime => (V2) Edm.DateTimeOffset => cds.Timestamp,
|
|
120
|
+
// cds.Timestamp should be converted into Edm.DateTimeOffset literal form `datetimeoffset'${new Date(value).toISOString()}'`
|
|
121
|
+
// However, odata-v2-proxy forwards it literaly as `datetimeoffset'...'` which is rejected by okra.
|
|
122
|
+
// Note that OData V2 spec example also does not contain 'datetimeoffset' predicate.
|
|
123
|
+
return new Date(value).toISOString()
|
|
124
|
+
default:
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
93
129
|
const _calculateTicksOffsetSum = text => {
|
|
94
130
|
return (text.replace(/\s/g, '').match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => {
|
|
95
131
|
return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000) // ticks are milliseconds (0), offset are minutes (1)
|
|
@@ -115,9 +151,14 @@ const _elementType = element => {
|
|
|
115
151
|
return type
|
|
116
152
|
}
|
|
117
153
|
|
|
118
|
-
const convertV2ResponseData = (data, target, ieee754Compatible) => {
|
|
154
|
+
const convertV2ResponseData = (data, target, ieee754Compatible, exponentialDecimals) => {
|
|
155
|
+
if (!target || !target.elements) return data
|
|
156
|
+
return _convertData(data, target, _convertValue(ieee754Compatible, exponentialDecimals))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const convertV2PayloadData = (data, target) => {
|
|
119
160
|
if (!target || !target.elements) return data
|
|
120
|
-
return _convertData(data, target,
|
|
161
|
+
return _convertData(data, target, _convertPayloadValue)
|
|
121
162
|
}
|
|
122
163
|
|
|
123
164
|
const deepSanitize = arg => {
|
|
@@ -132,5 +173,6 @@ const deepSanitize = arg => {
|
|
|
132
173
|
|
|
133
174
|
module.exports = {
|
|
134
175
|
convertV2ResponseData,
|
|
176
|
+
convertV2PayloadData,
|
|
135
177
|
deepSanitize
|
|
136
178
|
}
|
|
@@ -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
|
|
|
@@ -5,16 +5,14 @@ const { entriesStructureToEntityStructure } = require('./utils')
|
|
|
5
5
|
module.exports = async (service, entityFQN, selection) => {
|
|
6
6
|
const filter = getArgumentByName(selection.arguments, ARGUMENT.FILTER)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const queryBeforeUpdate = service.read(entityFQN)
|
|
9
9
|
queryBeforeUpdate.columns(astToColumns(selection.selectionSet.selections))
|
|
10
10
|
|
|
11
11
|
if (filter) {
|
|
12
12
|
queryBeforeUpdate.where(astToWhere(filter))
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
let query = service.update(entityFQN)
|
|
15
|
+
const query = service.update(entityFQN)
|
|
18
16
|
|
|
19
17
|
if (filter) {
|
|
20
18
|
query.where(astToWhere(filter))
|
|
@@ -24,7 +22,12 @@ module.exports = async (service, entityFQN, selection) => {
|
|
|
24
22
|
const entries = entriesStructureToEntityStructure(service, entityFQN, astToEntries(input))
|
|
25
23
|
query.with(entries)
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
let resultBeforeUpdate
|
|
26
|
+
const result = await service.tx(async tx => {
|
|
27
|
+
// read needs to be done before the update, otherwise the where clause might become invalid (case that properties in where clause are updated by the mutation)
|
|
28
|
+
resultBeforeUpdate = await service.tx(tx => tx.run(queryBeforeUpdate))
|
|
29
|
+
return tx.run(query)
|
|
30
|
+
})
|
|
28
31
|
|
|
29
32
|
// Merge selected fields with updated data
|
|
30
33
|
return resultBeforeUpdate.map(original => ({ ...original, ...result }))
|
|
@@ -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(',')
|