@sap/cds 6.1.3 → 6.2.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 +77 -8
- package/apis/cds.d.ts +18 -6
- package/apis/connect.d.ts +1 -1
- package/apis/cqn.d.ts +1 -1
- package/apis/log.d.ts +23 -5
- package/apis/ql.d.ts +128 -61
- package/apis/services.d.ts +11 -0
- package/apis/test.d.ts +61 -0
- package/apis/utils.d.ts +15 -0
- package/app/fiori/preview.js +1 -0
- package/bin/build/buildTaskEngine.js +70 -22
- package/bin/build/buildTaskFactory.js +18 -11
- package/bin/build/buildTaskHandler.js +1 -1
- package/bin/build/buildTaskProviderFactory.js +3 -13
- package/bin/build/constants.js +0 -1
- package/bin/build/index.js +14 -6
- package/bin/build/provider/buildTaskHandlerEdmx.js +2 -3
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -2
- package/bin/build/provider/buildTaskHandlerInternal.js +3 -6
- package/bin/build/provider/buildTaskProviderInternal.js +51 -39
- package/bin/build/provider/fiori/index.js +3 -3
- package/bin/build/provider/hana/2migration.js +1 -1
- package/bin/build/provider/hana/index.js +34 -27
- package/bin/build/provider/java/index.js +6 -7
- package/bin/build/provider/mtx/index.js +20 -18
- package/bin/build/provider/mtx/resourcesTarBuilder.js +8 -11
- package/bin/build/provider/mtx-sidecar/index.js +13 -17
- package/bin/build/provider/nodejs/index.js +8 -7
- package/bin/build/util.js +22 -4
- package/bin/cds.js +8 -4
- package/bin/deploy/to-hana/cfUtil.js +53 -18
- package/bin/mtx/in-cds.js +1 -0
- package/bin/serve.js +37 -30
- package/lib/auth/basic-auth.js +33 -0
- package/lib/auth/dummy-auth.js +7 -0
- package/lib/auth/ias-auth.js +2 -0
- package/lib/auth/index.js +31 -0
- package/lib/auth/jwt-auth.js +3 -0
- package/lib/auth/mocked-users.js +72 -0
- package/lib/auth/passport-basic.js +12 -0
- package/lib/auth/passport-digest.js +14 -0
- package/lib/auth/xsuaa-auth.js +3 -0
- package/lib/compile/cds-compile.js +3 -3
- package/lib/compile/to/cdl.js +5 -1
- package/lib/compile/to/edm.js +8 -0
- package/lib/compile/to/gql.js +1 -0
- package/lib/compile/to/json.js +30 -5
- package/lib/compile/to/sql.js +3 -1
- package/lib/core/index.js +5 -1
- package/lib/dbs/cds-deploy.js +36 -6
- package/lib/env/cds-env.js +15 -5
- package/lib/env/cds-requires.js +51 -58
- package/lib/env/defaults.js +1 -0
- package/lib/env/schemas/cds-package.json +4 -0
- package/lib/env/schemas/cds-rc.json +63 -77
- package/lib/i18n/localize.js +16 -5
- package/lib/index.js +9 -4
- package/lib/log/cds-error.js +4 -6
- package/lib/log/cds-log.js +89 -53
- package/lib/log/service/index.js +1 -0
- package/lib/ql/CREATE.js +2 -5
- package/lib/ql/DELETE.js +1 -1
- package/lib/ql/DROP.js +1 -3
- package/lib/ql/INSERT.js +3 -3
- package/lib/ql/Query.js +10 -23
- package/lib/ql/SELECT.js +1 -2
- package/lib/ql/UPDATE.js +2 -2
- package/lib/ql/Whereable.js +7 -15
- package/lib/ql/cds-ql.js +9 -3
- package/lib/req/cds-context.js +11 -3
- package/lib/req/context.js +29 -23
- package/lib/req/locale.js +9 -5
- package/lib/req/request.js +1 -0
- package/lib/req/user.js +2 -1
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/cds-serve.js +21 -14
- package/lib/srv/middlewares/cds-context.js +29 -0
- package/lib/srv/middlewares/ctx-model.js +24 -0
- package/lib/srv/middlewares/errors.js +9 -0
- package/lib/srv/middlewares/index.js +22 -0
- package/lib/srv/middlewares/sap-statistics.js +13 -0
- package/lib/srv/middlewares/trace.js +102 -0
- package/lib/srv/protocols/_legacy.js +42 -0
- package/lib/srv/protocols/graphql.js +39 -0
- package/lib/srv/protocols/hcql.js +37 -0
- package/lib/srv/protocols/index.js +86 -0
- package/lib/srv/protocols/odata-v2-proxy.js +3767 -0
- package/lib/srv/protocols/odata-v2.js +26 -0
- package/lib/srv/protocols/odata-v4.js +16 -0
- package/lib/srv/protocols/rest.js +13 -0
- package/lib/srv/srv-api.js +5 -0
- package/lib/srv/srv-models.js +4 -6
- package/lib/utils/axios.js +3 -2
- package/lib/utils/cds-test.js +27 -21
- package/lib/utils/cds-utils.js +19 -20
- package/lib/utils/tar.js +175 -0
- package/libx/_runtime/audit/generic/personal/utils.js +18 -7
- package/libx/_runtime/audit/utils/v2.js +1 -0
- package/libx/_runtime/auth/index.js +4 -0
- package/libx/_runtime/auth/strategies/ias-auth.js +76 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +8 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +15 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +9 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriInfo.js +5 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +12 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/RequestValidator.js +47 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
- package/libx/_runtime/cds-services/util/assert.js +4 -0
- package/libx/_runtime/common/aspects/relation.js +1 -1
- package/libx/_runtime/common/composition/data.js +61 -15
- package/libx/_runtime/common/composition/delete.js +0 -1
- package/libx/_runtime/common/composition/insert.js +0 -1
- package/libx/_runtime/common/composition/tree.js +4 -10
- package/libx/_runtime/common/composition/update.js +44 -21
- package/libx/_runtime/common/generic/auth/capabilities.js +8 -10
- package/libx/_runtime/common/generic/crud.js +1 -2
- package/libx/_runtime/common/generic/etag.js +4 -4
- package/libx/_runtime/common/generic/input.js +4 -4
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/generic/put.js +3 -3
- package/libx/_runtime/common/generic/sorting.js +4 -4
- package/libx/_runtime/common/generic/temporal.js +3 -3
- package/libx/_runtime/common/i18n/messages.properties +0 -7
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +11 -6
- package/libx/_runtime/common/utils/csn.js +0 -28
- package/libx/_runtime/common/utils/draft.js +8 -1
- package/libx/_runtime/common/utils/path.js +7 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -3
- package/libx/_runtime/db/data-conversion/post-processing.js +3 -44
- package/libx/_runtime/db/generic/input.js +3 -3
- package/libx/_runtime/db/sql-builder/dataTypes.js +4 -0
- package/libx/_runtime/fiori/generic/activate.js +2 -2
- package/libx/_runtime/fiori/generic/before.js +40 -72
- package/libx/_runtime/fiori/generic/cancel.js +2 -2
- package/libx/_runtime/fiori/generic/delete.js +2 -2
- package/libx/_runtime/fiori/generic/edit.js +2 -2
- package/libx/_runtime/fiori/generic/new.js +2 -2
- package/libx/_runtime/fiori/generic/patch.js +49 -37
- package/libx/_runtime/fiori/generic/prepare.js +2 -2
- package/libx/_runtime/fiori/generic/read.js +27 -37
- package/libx/_runtime/fiori/utils/where.js +4 -2
- package/libx/_runtime/hana/Service.js +1 -3
- package/libx/_runtime/hana/conversion.js +3 -0
- package/libx/_runtime/hana/driver.js +33 -3
- package/libx/_runtime/hana/dynatrace.js +1 -0
- package/libx/_runtime/hana/search2Contains.js +12 -1
- package/libx/_runtime/hana/search2cqn4sql.js +10 -27
- package/libx/_runtime/hana/streaming.js +1 -0
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +5 -2
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +62 -3
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/redis-messaging.js +1 -0
- package/libx/_runtime/remote/Service.js +2 -2
- package/libx/_runtime/remote/utils/client.js +8 -3
- package/libx/_runtime/remote/utils/data.js +7 -2
- package/libx/_runtime/sqlite/Service.js +18 -7
- package/libx/_runtime/sqlite/conversion.js +3 -0
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +3 -3
- package/libx/_runtime/sqlite/localized.js +8 -8
- package/libx/odata/afterburner.js +39 -7
- package/libx/odata/cqn2odata.js +6 -3
- package/libx/odata/grammar.pegjs +66 -18
- package/libx/odata/index.js +3 -2
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +2 -0
- package/libx/rest/RestAdapter.js +62 -43
- package/libx/rest/middleware/parse.js +2 -1
- package/libx/rest/middleware/update.js +1 -1
- package/package.json +2 -2
- package/server.js +5 -4
- package/srv/mtx.cds +1 -1
- package/srv/mtx.js +4 -33
- package/lib/srv/adapters.js +0 -85
- package/lib/utils/resources/index.js +0 -48
- package/lib/utils/resources/tar.js +0 -49
- package/lib/utils/resources/utils.js +0 -11
- package/libx/_runtime/extensibility/activate.js +0 -69
- package/libx/_runtime/extensibility/add.js +0 -50
- package/libx/_runtime/extensibility/addExtension.js +0 -72
- package/libx/_runtime/extensibility/defaults.js +0 -34
- package/libx/_runtime/extensibility/handler/transformREAD.js +0 -121
- package/libx/_runtime/extensibility/handler/transformRESULT.js +0 -51
- package/libx/_runtime/extensibility/handler/transformWRITE.js +0 -64
- package/libx/_runtime/extensibility/linter/allowlist_checker.js +0 -373
- package/libx/_runtime/extensibility/linter/annotations_checker.js +0 -113
- package/libx/_runtime/extensibility/linter/checker_base.js +0 -20
- package/libx/_runtime/extensibility/linter/namespace_checker.js +0 -180
- package/libx/_runtime/extensibility/linter.js +0 -32
- package/libx/_runtime/extensibility/push.js +0 -118
- package/libx/_runtime/extensibility/service.js +0 -38
- package/libx/_runtime/extensibility/token.js +0 -57
- package/libx/_runtime/extensibility/utils.js +0 -131
- package/libx/_runtime/extensibility/validation.js +0 -50
- package/libx/_runtime/extensibility/views.js +0 -12
- package/srv/extensibility-service.cds +0 -60
- package/srv/extensibility-service.js +0 -1
- package/srv/extensions.cds +0 -8
- package/srv/model-provider.cds +0 -61
- package/srv/model-provider.js +0 -143
|
@@ -0,0 +1,3767 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// OData V2/V4 Delta: http://docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/new-in-odata-v4.0-cn01.html
|
|
4
|
+
|
|
5
|
+
const URL = require("url");
|
|
6
|
+
const express = require("express"); // eslint-disable-line cds/no-missing-dependencies
|
|
7
|
+
const expressFileUpload = require("express-fileupload"); // eslint-disable-line cds/no-missing-dependencies
|
|
8
|
+
const fetch = require("node-fetch"); // eslint-disable-line cds/no-missing-dependencies
|
|
9
|
+
const cds = require("../../index"); // eslint-disable-line cds/no-missing-dependencies
|
|
10
|
+
const { promisify } = require("util");
|
|
11
|
+
const { createProxyMiddleware } = require("http-proxy-middleware"); // eslint-disable-line cds/no-missing-dependencies
|
|
12
|
+
|
|
13
|
+
const LOG = cds.log("cov2ap");
|
|
14
|
+
|
|
15
|
+
const SeverityMap = {
|
|
16
|
+
1: "success",
|
|
17
|
+
2: "info",
|
|
18
|
+
3: "warning",
|
|
19
|
+
4: "error",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// NOTE: we want to support HANA's SYSUUID, which does not conform to real UUID formats
|
|
23
|
+
const UUIDLikeRegex = /guid'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/gi;
|
|
24
|
+
// https://www.w3.org/TR/xmlschema11-2/#nt-duDTFrag
|
|
25
|
+
const DurationRegex =
|
|
26
|
+
/^P(?:(\d)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?T(?:(\d{1,2})H)?(?:(\d{2})M)?(?:(\d{2}(?:\.\d+)?)S)?$/i;
|
|
27
|
+
// Unsupported Draft Filter
|
|
28
|
+
const UnsupportedDraftFilterRegex =
|
|
29
|
+
/\(IsActiveEntity eq true and (.*?)\) or \(IsActiveEntity eq false and \((.*?) or HasActiveEntity eq false\)\)/;
|
|
30
|
+
|
|
31
|
+
// https://cap.cloud.sap/docs/cds/types
|
|
32
|
+
const DataTypeMap = {
|
|
33
|
+
"cds.UUID": { v2: `guid'$1'`, v4: UUIDLikeRegex },
|
|
34
|
+
// "cds.Boolean" - no transformation
|
|
35
|
+
// "cds.Integer" - no transformation
|
|
36
|
+
"cds.Integer64": { v2: `$1L`, v4: /([-]?[0-9]+?)L/gi },
|
|
37
|
+
"cds.Decimal": { v2: `$1m`, v4: /([-]?[0-9]+?\.?[0-9]*)m/gi },
|
|
38
|
+
"cds.DecimalFloat": { v2: `$1f`, v4: /([-]?[0-9]+?\.?[0-9]*)f/gi },
|
|
39
|
+
"cds.Double": { v2: `$1d`, v4: /([-]?[0-9]+?\.?[0-9]*(?:E[+-]?[0-9]+?)?)d/gi },
|
|
40
|
+
"cds.Date": { v2: `datetime'$1'`, v4: /datetime'(.+?)'/gi },
|
|
41
|
+
"cds.Time": { v2: `time'$1'`, v4: /time'(.+?)'/gi },
|
|
42
|
+
"cds.DateTime": { v2: `datetimeoffset'$1'`, v4: /datetime(?:offset)?'(.+?)'/gi },
|
|
43
|
+
"cds.Timestamp": { v2: `datetimeoffset'$1'`, v4: /datetime(?:offset)?'(.+?)'/gi },
|
|
44
|
+
"cds.String": { v2: `'$1'`, v4: /(.*)/gis },
|
|
45
|
+
"cds.Binary": { v2: `binary'$1'`, v4: /X'(?:[0-9a-f][0-9a-f])+?'/gi },
|
|
46
|
+
"cds.LargeBinary": { v2: `binary'$1'`, v4: /X'(?:[0-9a-f][0-9a-f])+?'/gi },
|
|
47
|
+
"cds.LargeString": { v2: `'$1'`, v4: /(.*)/gis },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// https://www.odata.org/documentation/odata-version-2-0/overview/ (6. Primitive Data Types)
|
|
51
|
+
// https://cap.cloud.sap/docs/advanced/odata#type-mapping
|
|
52
|
+
const DataTypeOData = {
|
|
53
|
+
Binary: "cds.Binary",
|
|
54
|
+
Boolean: "cds.Boolean",
|
|
55
|
+
Byte: "cds.Integer",
|
|
56
|
+
DateTime: "cds.DateTime",
|
|
57
|
+
Decimal: "cds.Decimal",
|
|
58
|
+
Double: "cds.Double",
|
|
59
|
+
Single: "cds.Double",
|
|
60
|
+
Guid: "cds.UUID",
|
|
61
|
+
Int16: "cds.Integer",
|
|
62
|
+
Int32: "cds.Integer",
|
|
63
|
+
Int64: "cds.Integer64",
|
|
64
|
+
SByte: "cds.Integer",
|
|
65
|
+
String: "cds.String",
|
|
66
|
+
Time: "cds.Time",
|
|
67
|
+
DateTimeOffset: "cds.Timestamp",
|
|
68
|
+
Date: "cds.Date",
|
|
69
|
+
TimeOfDay: "cds.Time",
|
|
70
|
+
_Decimal: "cds.DecimalFloat",
|
|
71
|
+
_Binary: "cds.LargeBinary",
|
|
72
|
+
_String: "cds.LargeString",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const AggregationMap = {
|
|
76
|
+
SUM: "sum",
|
|
77
|
+
MIN: "min",
|
|
78
|
+
MAX: "max",
|
|
79
|
+
AVG: "average",
|
|
80
|
+
COUNT: "countdistinct",
|
|
81
|
+
COUNT_DISTINCT: "countdistinct",
|
|
82
|
+
$COUNT: "$count",
|
|
83
|
+
NONE: "none",
|
|
84
|
+
NOP: "nop",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const DefaultAggregation = AggregationMap.SUM;
|
|
88
|
+
|
|
89
|
+
const FilterFunctions = {
|
|
90
|
+
"substringof($,$)": "contains($2,$1)",
|
|
91
|
+
"gettotaloffsetminutes($)": "totaloffsetminutes($1)",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const FilterFunctionsCaseInsensitive = {
|
|
95
|
+
"substringof($,$)": "contains(tolower($2),tolower($1))",
|
|
96
|
+
"startswith($,$)": "startswith(tolower($1),tolower($2))",
|
|
97
|
+
"endswith($,$)": "endswith(tolower($1),tolower($2))",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const ProcessingDirection = {
|
|
101
|
+
Request: "req",
|
|
102
|
+
Response: "res",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const DefaultHost = "localhost";
|
|
106
|
+
const DefaultPort = 4004;
|
|
107
|
+
const DefaultTenant = "00000000-0000-0000-0000-000000000000";
|
|
108
|
+
const AggregationPrefix = "__AGGREGATION__";
|
|
109
|
+
const IEEE754Compatible = "IEEE754Compatible=true";
|
|
110
|
+
|
|
111
|
+
function convertToNodeHeaders(webHeaders) {
|
|
112
|
+
return Array.from(webHeaders.entries()).reduce((result, [key, value]) => {
|
|
113
|
+
result[key] = value;
|
|
114
|
+
return result;
|
|
115
|
+
}, {});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Instantiates a CDS OData V2 Adapter Proxy Express Router for a CDS-based OData V4 Server:
|
|
120
|
+
* @param {object} options CDS OData V2 Adapter Proxy options object.
|
|
121
|
+
* @param {string} options.base Base path under which the service is reachable. Default is ''.
|
|
122
|
+
* @param {string} options.path Path under which the proxy is reachable. Default is 'v2'.
|
|
123
|
+
* @param {string|string[]|object} options.model CDS service model (path(s) or CSN). Default is 'all'.
|
|
124
|
+
* @param {number} options.port Target port which points to OData V4 backend port. Default is process.env.PORT or 4004.
|
|
125
|
+
* @param {string} options.target Target which points to OData V4 backend host:port. Use 'auto' to infer the target from server url after listening. Default is e.g. 'http://localhost:4004'.
|
|
126
|
+
* @param {string} options.targetPath Target path to which is redirected. Default is ''.
|
|
127
|
+
* @param {object} options.services Service mapping object from url path name to service name. Default is {}.
|
|
128
|
+
* @param {boolean} options.mtxRemote CDS model is retrieved remotely via MTX endpoint for multitenant scenario (old MTX only). Default is false.
|
|
129
|
+
* @param {string} options.mtxEndpoint Endpoint to retrieve MTX metadata when option 'mtxRemote' is active (old MTX only). Default is '/mtx/v1'.
|
|
130
|
+
* @param {boolean} options.ieee754Compatible Edm.Decimal and Edm.Int64 are serialized IEEE754 compatible. Default is true.
|
|
131
|
+
* @param {number} options.fileUploadSizeLimit File upload file size limit (in bytes). Default is 10485760 (10 MB).
|
|
132
|
+
* @param {boolean} options.continueOnError Indicates to OData V4 backend to continue on error. Default is false.
|
|
133
|
+
* @param {boolean} options.isoTime Use ISO 8601 format for type cds.Time (Edm.Time). Default is false.
|
|
134
|
+
* @param {boolean} options.isoDate Use ISO 8601 format for type cds.Date (Edm.DateTime). Default is false.
|
|
135
|
+
* @param {boolean} options.isoDateTime Use ISO 8601 format for type cds.DateTime (Edm.DateTimeOffset). Default is false.
|
|
136
|
+
* @param {boolean} options.isoTimestamp Use ISO 8601 format for type cds.Timestamp (Edm.DateTimeOffset). Default is false.
|
|
137
|
+
* @param {boolean} options.isoDateTimeOffset Use ISO 8601 format for type Edm.DateTimeOffset (cds.DateTime, cds.Timestamp). Default is false.
|
|
138
|
+
* @param {string} options.bodyParserLimit Request and response body parser size limit. Default is '100mb'.
|
|
139
|
+
* @param {boolean} options.returnCollectionNested Collection of entity type is returned nested into a results section. Default is true.
|
|
140
|
+
* @param {boolean} options.returnComplexNested Function import return structure of complex type (non collection) is nested using function import name. Default is true.
|
|
141
|
+
* @param {boolean} options.returnPrimitiveNested Function import return structure of primitive type (non collection) is nested using function import name. Default is true.
|
|
142
|
+
* @param {boolean} options.returnPrimitivePlain Function import return value of primitive type is rendered as plain JSON value. Default is true.
|
|
143
|
+
* @param {string} options.messageTargetDefault Specifies the message target default, if target is undefined. Default is '/#TRANSIENT#'.
|
|
144
|
+
* @param {boolean} options.caseInsensitive: Transforms search functions i.e. substringof, startswith, endswith to case-insensitive variant. Default is false.
|
|
145
|
+
* @param {boolean} options.propagateMessageToDetails: Propagates root error or message always to details section. Default is false.
|
|
146
|
+
* @param {boolean} options.contentDisposition: Default content disposition for media streams (inline, attachment), if not available or calculated. Default is 'attachment'.
|
|
147
|
+
* @param {boolean} options.calcContentDisposition: Calculate content disposition for media streams even if already available. Default is false.
|
|
148
|
+
* @param {boolean} options.quoteSearch: Specifies if search expression is quoted automatically. Default is true.
|
|
149
|
+
* @param {boolean} options.fixDraftRequests: Specifies if unsupported draft requests are converted to a working version. Default is false.
|
|
150
|
+
* @returns {express.Router} CDS OData V2 Adapter Proxy Express Router
|
|
151
|
+
*/
|
|
152
|
+
function cov2ap(options = {}) {
|
|
153
|
+
const optionWithFallback = (name, fallback) => {
|
|
154
|
+
if (options && Object.prototype.hasOwnProperty.call(options, name)) {
|
|
155
|
+
return options[name];
|
|
156
|
+
}
|
|
157
|
+
if (cds.env.cov2ap && Object.prototype.hasOwnProperty.call(cds.env.cov2ap, name)) {
|
|
158
|
+
return cds.env.cov2ap[name];
|
|
159
|
+
}
|
|
160
|
+
return fallback;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const proxyCache = {};
|
|
164
|
+
const router = express.Router();
|
|
165
|
+
const base = optionWithFallback("base", "");
|
|
166
|
+
const _with_leading_slash = s => s.replace(/^([^/])/,'/$1')
|
|
167
|
+
const path = _with_leading_slash(optionWithFallback("path", "/v2"))
|
|
168
|
+
const sourcePath = _with_leading_slash(optionWithFallback("sourcePath", `${base ? "/" + base : ""}/${path}`))
|
|
169
|
+
const targetPath = _with_leading_slash(optionWithFallback("targetPath", ""))
|
|
170
|
+
const pathRewrite = { [`^${sourcePath}`]: targetPath };
|
|
171
|
+
let port = optionWithFallback("port", process.env.PORT || DefaultPort);
|
|
172
|
+
let target = optionWithFallback("target", `http://${DefaultHost}:${port}`);
|
|
173
|
+
const logLevel = optionWithFallback("logLevel", 'info');
|
|
174
|
+
const services = optionWithFallback("services", {});
|
|
175
|
+
const mtxRemote = optionWithFallback("mtxRemote", false);
|
|
176
|
+
const mtxEndpoint = optionWithFallback("mtxEndpoint", "/mtx/v1");
|
|
177
|
+
const ieee754Compatible = optionWithFallback("ieee754Compatible", true);
|
|
178
|
+
const fileUploadSizeLimit = optionWithFallback("fileUploadSizeLimit", 10 * 1024 * 1024);
|
|
179
|
+
const continueOnError = optionWithFallback("continueOnError", false);
|
|
180
|
+
const isoTime = optionWithFallback("isoTime", false);
|
|
181
|
+
const isoDate = optionWithFallback("isoDate", false);
|
|
182
|
+
const isoDateTime = optionWithFallback("isoDateTime", false);
|
|
183
|
+
const isoTimestamp = optionWithFallback("isoTimestamp", false);
|
|
184
|
+
const isoDateTimeOffset = optionWithFallback("isoDateTimeOffset", false);
|
|
185
|
+
const bodyParserLimit = optionWithFallback("bodyParserLimit", "100mb");
|
|
186
|
+
const returnCollectionNested = optionWithFallback("returnCollectionNested", true);
|
|
187
|
+
const returnComplexNested = optionWithFallback("returnComplexNested", true);
|
|
188
|
+
const returnPrimitiveNested = optionWithFallback("returnPrimitiveNested", true);
|
|
189
|
+
const returnPrimitivePlain = optionWithFallback("returnPrimitivePlain", true);
|
|
190
|
+
const messageTargetDefault = optionWithFallback("messageTargetDefault", "/#TRANSIENT#");
|
|
191
|
+
const caseInsensitive = optionWithFallback("caseInsensitive", false);
|
|
192
|
+
const propagateMessageToDetails = optionWithFallback("propagateMessageToDetails", false);
|
|
193
|
+
const contentDisposition = optionWithFallback("contentDisposition", "attachment");
|
|
194
|
+
const calcContentDisposition = optionWithFallback("calcContentDisposition", false);
|
|
195
|
+
const quoteSearch = optionWithFallback("quoteSearch", true);
|
|
196
|
+
const fixDraftRequests = optionWithFallback("fixDraftRequests", false);
|
|
197
|
+
|
|
198
|
+
if (caseInsensitive) {
|
|
199
|
+
Object.assign(FilterFunctions, FilterFunctionsCaseInsensitive);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const fileUpload = expressFileUpload({
|
|
203
|
+
abortOnLimit: true,
|
|
204
|
+
limits: {
|
|
205
|
+
files: 1,
|
|
206
|
+
fileSize: fileUploadSizeLimit,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
let model = optionWithFallback("model", "all");
|
|
211
|
+
if (Array.isArray(model)) {
|
|
212
|
+
model = model.map((entry) => (entry === "all" ? "*" : entry));
|
|
213
|
+
} else {
|
|
214
|
+
model = model === "all" ? "*" : model;
|
|
215
|
+
}
|
|
216
|
+
model = cds.resolve(model);
|
|
217
|
+
|
|
218
|
+
cds.on("serving", (service) => {
|
|
219
|
+
const isOData = Object.keys(service._adapters).find((adapter) => adapter.startsWith("odata"));
|
|
220
|
+
if (!isOData) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const provider = (entity) => {
|
|
224
|
+
const href = `${sourcePath}${service.path}/${entity || "$metadata"}`;
|
|
225
|
+
return { href, name: `${entity || "$metadata"} (V2)`, title: "OData V2" };
|
|
226
|
+
};
|
|
227
|
+
service.$linkProviders = service.$linkProviders || [];
|
|
228
|
+
service.$linkProviders.push(provider);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (cds.mtx && cds.mtx.eventEmitter && cds.env.requires && cds.env.requires.multitenancy) {
|
|
232
|
+
cds.mtx.eventEmitter.on(cds.mtx.events.TENANT_UPDATED, (tenant) => {
|
|
233
|
+
delete proxyCache[tenant];
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// TODO: Cache invalidation for Streamlined MTX (when extensibility is supported)
|
|
237
|
+
|
|
238
|
+
router.use(`${path}/:service`, async (req, res, next) => {
|
|
239
|
+
req.contextId =
|
|
240
|
+
req.headers["x-correlation-id"] ||
|
|
241
|
+
req.headers["x-correlationid"] ||
|
|
242
|
+
req.headers["x-request-id"] ||
|
|
243
|
+
req.headers["x-vcap-request-id"] ||
|
|
244
|
+
cds.utils.uuid();
|
|
245
|
+
res.set("x-request-id", req.contextId);
|
|
246
|
+
res.set("x-correlation-id", req.contextId);
|
|
247
|
+
res.set("x-correlationid", req.contextId);
|
|
248
|
+
try {
|
|
249
|
+
const [authType, token] = (req.headers.authorization && req.headers.authorization.split(" ")) || [];
|
|
250
|
+
if (authType && token) {
|
|
251
|
+
let jwtBody;
|
|
252
|
+
switch (authType) {
|
|
253
|
+
case "Basic":
|
|
254
|
+
req.user = {
|
|
255
|
+
id: decodeBase64(token).split(":")[0],
|
|
256
|
+
};
|
|
257
|
+
if (req.user.id && cds.env.requires.auth && cds.env.requires.auth.strategy === "mock") {
|
|
258
|
+
const user = (cds.env.requires.auth.users || {})[req.user.id];
|
|
259
|
+
req.tenant = user && (user.tenant || (user.jwt && user.jwt.zid));
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
case "Bearer":
|
|
263
|
+
jwtBody = decodeJwtTokenBody(token);
|
|
264
|
+
req.user = {
|
|
265
|
+
id: jwtBody.user_name || jwtBody.client_id,
|
|
266
|
+
};
|
|
267
|
+
req.tenant = jwtBody.zid;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
logError(req, "Authorization", err);
|
|
273
|
+
}
|
|
274
|
+
next();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
router.get(`${path}/*\\$metadata`, async (req, res) => {
|
|
278
|
+
let serviceValid = true;
|
|
279
|
+
try {
|
|
280
|
+
const metadataUrlPath = targetUrl(req);
|
|
281
|
+
|
|
282
|
+
// Trace
|
|
283
|
+
traceRequest(req, "Request", req.method, req.originalUrl, req.headers, req.body);
|
|
284
|
+
traceRequest(req, "ProxyRequest", req.method, metadataUrlPath, req.headers, req.body);
|
|
285
|
+
|
|
286
|
+
const result = await Promise.all([
|
|
287
|
+
fetch(target + metadataUrlPath, {
|
|
288
|
+
method: "GET",
|
|
289
|
+
headers: propagateHeaders(req),
|
|
290
|
+
}),
|
|
291
|
+
(async () => {
|
|
292
|
+
const { csn } = await getMetadata(req);
|
|
293
|
+
req.csn = csn;
|
|
294
|
+
const service = serviceFromRequest(req);
|
|
295
|
+
if (service && service.name) {
|
|
296
|
+
serviceValid = service.valid;
|
|
297
|
+
const { edmx } = await getMetadata(req, service.name);
|
|
298
|
+
return edmx;
|
|
299
|
+
}
|
|
300
|
+
})(),
|
|
301
|
+
]);
|
|
302
|
+
const [metadataResponse, edmx] = result;
|
|
303
|
+
const headers = convertBasicHeaders(convertToNodeHeaders(metadataResponse.headers));
|
|
304
|
+
delete headers["content-encoding"];
|
|
305
|
+
const metadataBody = await metadataResponse.text();
|
|
306
|
+
let body;
|
|
307
|
+
if (metadataResponse.ok) {
|
|
308
|
+
body = edmx;
|
|
309
|
+
} else {
|
|
310
|
+
body = metadataBody;
|
|
311
|
+
}
|
|
312
|
+
setContentLength(headers, body);
|
|
313
|
+
|
|
314
|
+
// Trace
|
|
315
|
+
traceResponse(
|
|
316
|
+
req,
|
|
317
|
+
"Proxy Response",
|
|
318
|
+
metadataResponse.status,
|
|
319
|
+
metadataResponse.statusMessage,
|
|
320
|
+
headers,
|
|
321
|
+
metadataBody
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
respond(req, res, metadataResponse.status, headers, body);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (serviceValid) {
|
|
327
|
+
// Error
|
|
328
|
+
logError(req, "MetadataRequest", err);
|
|
329
|
+
res.status(500).send("Internal Server Error");
|
|
330
|
+
} else {
|
|
331
|
+
res.status(404).send("Not Found");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
router.use(`${path}/:service`,
|
|
337
|
+
|
|
338
|
+
// Body Parsers
|
|
339
|
+
(req, res, next) => {
|
|
340
|
+
const contentType = req.header("content-type");
|
|
341
|
+
if (!contentType) {
|
|
342
|
+
return next();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isApplicationJSON(contentType)) {
|
|
346
|
+
express.json({ limit: bodyParserLimit })(req, res, next);
|
|
347
|
+
} else if (isMultipartMixed(contentType)) {
|
|
348
|
+
express.text({ type: "multipart/mixed", limit: bodyParserLimit })(req, res, next);
|
|
349
|
+
} else {
|
|
350
|
+
req.checkUploadBinary = req.method === "POST";
|
|
351
|
+
next();
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
// Inject Context
|
|
356
|
+
async (req, res, next) => {
|
|
357
|
+
try {
|
|
358
|
+
const { csn } = await getMetadata(req);
|
|
359
|
+
req.csn = csn;
|
|
360
|
+
} catch (err) {
|
|
361
|
+
// Error
|
|
362
|
+
logError(req, "Request", err);
|
|
363
|
+
res.status(500).send("Internal Server Error");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const service = serviceFromRequest(req);
|
|
367
|
+
req.base = base;
|
|
368
|
+
req.service = service.name;
|
|
369
|
+
req.servicePath = service.path;
|
|
370
|
+
req.context = {};
|
|
371
|
+
req.contexts = [];
|
|
372
|
+
req.contentId = {};
|
|
373
|
+
req.lookupContext = {};
|
|
374
|
+
next();
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
// File Upload
|
|
378
|
+
async (req, res, next) => {
|
|
379
|
+
if (!req.checkUploadBinary) {
|
|
380
|
+
return next();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const targetPath = targetUrl(req);
|
|
384
|
+
const url = parseUrl(targetPath, req);
|
|
385
|
+
const definition = contextFromUrl(url, req);
|
|
386
|
+
if (!definition) {
|
|
387
|
+
return next();
|
|
388
|
+
}
|
|
389
|
+
const elements = definitionElements(definition);
|
|
390
|
+
const mediaDataElementName =
|
|
391
|
+
findElementByAnnotation(elements, "@Core.MediaType") ||
|
|
392
|
+
findElementByType(elements, DataTypeOData._Binary, req) ||
|
|
393
|
+
findElementByType(elements, DataTypeOData.Binary, req);
|
|
394
|
+
if (!mediaDataElementName) {
|
|
395
|
+
return next();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const handleMediaEntity = async (contentType, filename, headers = {}) => {
|
|
399
|
+
try {
|
|
400
|
+
contentType = contentType || "application/octet-stream";
|
|
401
|
+
const body = {};
|
|
402
|
+
// Custom body
|
|
403
|
+
const caseInsensitiveElements = structureKeys(elements).reduce((result, name) => {
|
|
404
|
+
result[name.toLowerCase()] = elements[name];
|
|
405
|
+
return result;
|
|
406
|
+
}, {});
|
|
407
|
+
Object.keys(headers).forEach((name) => {
|
|
408
|
+
const element = caseInsensitiveElements[name.toLowerCase()];
|
|
409
|
+
if (element) {
|
|
410
|
+
const value = convertDataTypeToV4(headers[name], elementType(element, req), definition, headers);
|
|
411
|
+
body[element.name] = decodeHeaderValue(definition, element, element.name, value);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
const mediaDataElement = elements[mediaDataElementName];
|
|
415
|
+
const mediaTypeElementName =
|
|
416
|
+
(mediaDataElement["@Core.MediaType"] && mediaDataElement["@Core.MediaType"]["="]) ||
|
|
417
|
+
findElementByAnnotation(elements, "@Core.IsMediaType");
|
|
418
|
+
if (mediaTypeElementName) {
|
|
419
|
+
body[mediaTypeElementName] = contentType;
|
|
420
|
+
}
|
|
421
|
+
const contentDispositionFilenameElementName =
|
|
422
|
+
findElementValueByAnnotation(elements, "@Core.ContentDisposition.Filename") ||
|
|
423
|
+
findElementValueByAnnotation(elements, "@Common.ContentDisposition.Filename");
|
|
424
|
+
if (contentDispositionFilenameElementName && filename) {
|
|
425
|
+
const element = elements[contentDispositionFilenameElementName];
|
|
426
|
+
body[contentDispositionFilenameElementName] = decodeHeaderValue(
|
|
427
|
+
definition,
|
|
428
|
+
element,
|
|
429
|
+
element.name,
|
|
430
|
+
filename
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const url = target + targetPath;
|
|
434
|
+
const postHeaders = propagateHeaders(req, {
|
|
435
|
+
...headers,
|
|
436
|
+
"content-type": "application/json",
|
|
437
|
+
});
|
|
438
|
+
delete postHeaders["transfer-encoding"];
|
|
439
|
+
|
|
440
|
+
// Trace
|
|
441
|
+
traceRequest(req, "ProxyRequest", "POST", url, postHeaders, body);
|
|
442
|
+
|
|
443
|
+
const response = await fetch(url, {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: postHeaders,
|
|
446
|
+
body: JSON.stringify(body),
|
|
447
|
+
});
|
|
448
|
+
const responseBody = await response.json();
|
|
449
|
+
const responseHeaders = convertToNodeHeaders(response.headers);
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
res
|
|
452
|
+
.status(response.status)
|
|
453
|
+
.set({
|
|
454
|
+
"content-type": "application/json",
|
|
455
|
+
})
|
|
456
|
+
.send(convertResponseError(responseBody, responseHeaders, definition, req));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Rewrite
|
|
461
|
+
req.method = "PUT";
|
|
462
|
+
req.originalUrl += `(${entityKey(responseBody, definition, elements, req)})/${mediaDataElementName}`;
|
|
463
|
+
req.baseUrl = req.originalUrl;
|
|
464
|
+
req.overwriteResponse = {
|
|
465
|
+
kind: "uploadBinary",
|
|
466
|
+
statusCode: response.status,
|
|
467
|
+
headers: responseHeaders,
|
|
468
|
+
body: responseBody,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// Trace
|
|
472
|
+
traceResponse(req, "ProxyResponse", response.status, response.statusText, responseHeaders, responseBody);
|
|
473
|
+
|
|
474
|
+
next();
|
|
475
|
+
} catch (err) {
|
|
476
|
+
// Error
|
|
477
|
+
logError(req, "FileUpload", err);
|
|
478
|
+
res.status(500).send("Internal Server Error");
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const headers = req.headers;
|
|
483
|
+
if (isMultipartFormData(headers["content-type"])) {
|
|
484
|
+
fileUpload(req, res, async () => {
|
|
485
|
+
await handleMediaEntity(
|
|
486
|
+
req.body && req.body["content-type"],
|
|
487
|
+
req.body &&
|
|
488
|
+
(req.body["slug"] ||
|
|
489
|
+
req.body["filename"] ||
|
|
490
|
+
contentDispositionFilename(req.body) ||
|
|
491
|
+
contentDispositionFilename(headers) ||
|
|
492
|
+
req.body["name"]),
|
|
493
|
+
req.body
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
} else {
|
|
497
|
+
await handleMediaEntity(
|
|
498
|
+
headers["content-type"],
|
|
499
|
+
headers["slug"] || headers["filename"] || contentDispositionFilename(headers) || headers["name"],
|
|
500
|
+
headers
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// Proxy Middleware
|
|
507
|
+
function setupProxyMiddleware() {
|
|
508
|
+
return createProxyMiddleware({
|
|
509
|
+
target,
|
|
510
|
+
changeOrigin: true,
|
|
511
|
+
selfHandleResponse: true,
|
|
512
|
+
logLevel,
|
|
513
|
+
pathRewrite,
|
|
514
|
+
onProxyReq: (proxyReq, req, res) => {
|
|
515
|
+
convertProxyRequest(proxyReq, req, res);
|
|
516
|
+
},
|
|
517
|
+
onProxyRes: (proxyRes, req, res) => {
|
|
518
|
+
convertProxyResponse(proxyRes, req, res);
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (target === "auto") {
|
|
524
|
+
cds.on("listening", ({ server, url }) => {
|
|
525
|
+
port = server.address().port;
|
|
526
|
+
target = url;
|
|
527
|
+
router.use(`${path}/*`, setupProxyMiddleware());
|
|
528
|
+
});
|
|
529
|
+
} else {
|
|
530
|
+
router.use(`${path}/*`, setupProxyMiddleware());
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function contentDispositionFilename(headers) {
|
|
534
|
+
const contentDispositionHeader = headers["content-disposition"] || headers["Content-Disposition"];
|
|
535
|
+
if (contentDispositionHeader) {
|
|
536
|
+
const filenameMatch = contentDispositionHeader.match(/^.*filename="(.*)"$/is);
|
|
537
|
+
return filenameMatch && filenameMatch.pop();
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function decodeHeaderValue(entity, element, name, value) {
|
|
543
|
+
if (value === undefined || value === null || value === "" || typeof value !== "string") {
|
|
544
|
+
return value;
|
|
545
|
+
}
|
|
546
|
+
let decodes = [];
|
|
547
|
+
if (Array.isArray(element["@cov2ap.headerDecode"])) {
|
|
548
|
+
decodes = element["@cov2ap.headerDecode"];
|
|
549
|
+
} else if (typeof element["@cov2ap.headerDecode"] === "string") {
|
|
550
|
+
decodes = [element["@cov2ap.headerDecode"]];
|
|
551
|
+
}
|
|
552
|
+
if (decodes.length > 0) {
|
|
553
|
+
decodes.forEach((decode) => {
|
|
554
|
+
switch (decode.toLowerCase()) {
|
|
555
|
+
case "uri":
|
|
556
|
+
value = decodeURI(value);
|
|
557
|
+
break;
|
|
558
|
+
case "uricomponent":
|
|
559
|
+
value = decodeURIComponent(value);
|
|
560
|
+
break;
|
|
561
|
+
case "base64":
|
|
562
|
+
value = decodeBase64(value);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function serviceFromRequest(req) {
|
|
571
|
+
let serviceName;
|
|
572
|
+
let serviceValid = true;
|
|
573
|
+
let servicePath = req.params.service
|
|
574
|
+
let path = '/'+ servicePath
|
|
575
|
+
Object.keys(services).find((path) => {
|
|
576
|
+
if (servicePath.toLowerCase().startsWith(normalizeSlashes(path).toLowerCase())) {
|
|
577
|
+
serviceName = services[path];
|
|
578
|
+
servicePath = stripSlashes(path);
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
});
|
|
583
|
+
if (!serviceName) {
|
|
584
|
+
let srv = cds.service.providers.find(service => service.path === path)
|
|
585
|
+
if (srv) serviceName = srv.name
|
|
586
|
+
}
|
|
587
|
+
if (!serviceName) {
|
|
588
|
+
let srv = cds.service.providers.find(service => service.path === "/")
|
|
589
|
+
if (srv) servicePath = ""
|
|
590
|
+
}
|
|
591
|
+
if (!serviceName || !req.csn.definitions[serviceName] || req.csn.definitions[serviceName].kind !== "service") {
|
|
592
|
+
logWarning(req, "Service", "Service definition not found for request path", {
|
|
593
|
+
requestPath: servicePath,
|
|
594
|
+
serviceName,
|
|
595
|
+
});
|
|
596
|
+
serviceValid = false;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
name: serviceName,
|
|
600
|
+
path: servicePath,
|
|
601
|
+
valid: serviceValid,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function getMetadata(req, service) {
|
|
606
|
+
let metadata;
|
|
607
|
+
if (req.tenant) {
|
|
608
|
+
if (mtxRemote && mtxEndpoint) {
|
|
609
|
+
metadata = await getTenantMetadataRemote(req, service);
|
|
610
|
+
} else if (cds.mtx && cds.env.requires && cds.env.requires.multitenancy) {
|
|
611
|
+
metadata = await getTenantMetadataLocal(req, service);
|
|
612
|
+
} else if (cds.env.requires && cds.env.requires["cds.xt.ModelProviderService"]) {
|
|
613
|
+
metadata = await getTenantMetadataStreamlined(req, service);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (!metadata) {
|
|
617
|
+
metadata = await getDefaultMetadata(req, service);
|
|
618
|
+
}
|
|
619
|
+
return metadata;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function getTenantMetadataRemote(req, service) {
|
|
623
|
+
const mtxBasePath =
|
|
624
|
+
mtxEndpoint.startsWith("http://") || mtxEndpoint.startsWith("https://") ? mtxEndpoint : `${target}${mtxEndpoint}`;
|
|
625
|
+
return await prepareMetadata(
|
|
626
|
+
req.tenant,
|
|
627
|
+
async (tenant) => {
|
|
628
|
+
const response = await fetch(`${mtxBasePath}/metadata/csn/${tenant}`, {
|
|
629
|
+
method: "GET",
|
|
630
|
+
headers: propagateHeaders(req),
|
|
631
|
+
});
|
|
632
|
+
if (!response.ok) {
|
|
633
|
+
throw new Error(await response.text());
|
|
634
|
+
}
|
|
635
|
+
return response.json();
|
|
636
|
+
},
|
|
637
|
+
async (tenant, service, locale) => {
|
|
638
|
+
const response = await fetch(
|
|
639
|
+
`${mtxBasePath}/metadata/edmx/${tenant}?name=${service}&language=${locale}&odataVersion=v2`,
|
|
640
|
+
{
|
|
641
|
+
method: "GET",
|
|
642
|
+
headers: propagateHeaders(req),
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
if (!response.ok) {
|
|
646
|
+
throw new Error(await response.text());
|
|
647
|
+
}
|
|
648
|
+
return response.text();
|
|
649
|
+
},
|
|
650
|
+
service,
|
|
651
|
+
determineLocale(req)
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function getTenantMetadataLocal(req, service) {
|
|
656
|
+
proxyCache[req.tenant] = proxyCache[req.tenant] || {};
|
|
657
|
+
const isExtended = await callCached(proxyCache[req.tenant], "isExtended", () => {
|
|
658
|
+
return cds.mtx.isExtended(req.tenant);
|
|
659
|
+
});
|
|
660
|
+
if (isExtended) {
|
|
661
|
+
return await prepareMetadata(
|
|
662
|
+
req.tenant,
|
|
663
|
+
async (tenant) => {
|
|
664
|
+
return await cds.mtx.getCsn(tenant);
|
|
665
|
+
},
|
|
666
|
+
async (tenant, service, locale) => {
|
|
667
|
+
return await cds.mtx.getEdmx(tenant, service, locale, "v2");
|
|
668
|
+
},
|
|
669
|
+
service,
|
|
670
|
+
determineLocale(req)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function getTenantMetadataStreamlined(req, service) {
|
|
676
|
+
const { "cds.xt.ModelProviderService": mps } = cds.services;
|
|
677
|
+
proxyCache[req.tenant] = proxyCache[req.tenant] || {};
|
|
678
|
+
const isExtended = await callCached(proxyCache[req.tenant], "isExtended", () => {
|
|
679
|
+
return mps.isExtended(req.tenant);
|
|
680
|
+
});
|
|
681
|
+
if (isExtended) {
|
|
682
|
+
return await prepareMetadata(
|
|
683
|
+
req.tenant,
|
|
684
|
+
async (tenant) => {
|
|
685
|
+
return await mps.getCsn(tenant, ensureArray(req.features), "nodejs"); // TODO: getExtCsn()? (when extensibility is supported)
|
|
686
|
+
},
|
|
687
|
+
async (tenant, service, locale) => {
|
|
688
|
+
return await mps.getEdmx(tenant, ensureArray(req.features), service, locale, "v2", "nodejs");
|
|
689
|
+
},
|
|
690
|
+
service,
|
|
691
|
+
determineLocale(req)
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function getDefaultMetadata(req, service) {
|
|
697
|
+
return await prepareMetadata(
|
|
698
|
+
DefaultTenant,
|
|
699
|
+
async () => {
|
|
700
|
+
if (typeof model === "object" && !Array.isArray(model)) {
|
|
701
|
+
return model;
|
|
702
|
+
}
|
|
703
|
+
return await cds.load(model);
|
|
704
|
+
},
|
|
705
|
+
async () => {},
|
|
706
|
+
service,
|
|
707
|
+
determineLocale(req)
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function prepareMetadata(tenant, loadCsn, loadEdmx, service, locale) {
|
|
712
|
+
proxyCache[tenant] = proxyCache[tenant] || {};
|
|
713
|
+
const csn = await callCached(proxyCache[tenant], "csn", () => {
|
|
714
|
+
return prepareCSN(tenant, loadCsn);
|
|
715
|
+
});
|
|
716
|
+
if (!service) {
|
|
717
|
+
return { csn };
|
|
718
|
+
}
|
|
719
|
+
proxyCache[tenant].edmx = proxyCache[tenant].edmx || {};
|
|
720
|
+
proxyCache[tenant].edmx[service] = proxyCache[tenant].edmx[service] || {};
|
|
721
|
+
const edmx = await callCached(proxyCache[tenant].edmx[service], locale, () => {
|
|
722
|
+
return prepareEdmx(tenant, csn, loadEdmx, service, locale);
|
|
723
|
+
});
|
|
724
|
+
return { csn, edmx };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function prepareCSN(tenant, loadCsn) {
|
|
728
|
+
let csnRaw;
|
|
729
|
+
if (cds.server && cds.model && tenant === DefaultTenant) {
|
|
730
|
+
csnRaw = cds.model;
|
|
731
|
+
} else {
|
|
732
|
+
csnRaw = await loadCsn(tenant);
|
|
733
|
+
}
|
|
734
|
+
let csn;
|
|
735
|
+
if (cds.compile.for.nodejs) {
|
|
736
|
+
csn = cds.compile.for.nodejs(csnRaw);
|
|
737
|
+
} else {
|
|
738
|
+
csn = csnRaw.meta && csnRaw.meta.transformation === "odata" ? csnRaw : cds.linked(cds.compile.for.odata(csnRaw));
|
|
739
|
+
}
|
|
740
|
+
return csn;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function prepareEdmx(tenant, csn, loadEdmx, service, locale) {
|
|
744
|
+
let edmx;
|
|
745
|
+
if (tenant !== DefaultTenant) {
|
|
746
|
+
edmx = await loadEdmx(tenant, service, locale);
|
|
747
|
+
}
|
|
748
|
+
if (!edmx) {
|
|
749
|
+
edmx = await cds.compile.to.edmx(csn, {
|
|
750
|
+
service,
|
|
751
|
+
version: "v2",
|
|
752
|
+
});
|
|
753
|
+
edmx = cds.localize(csn, locale, edmx);
|
|
754
|
+
}
|
|
755
|
+
return edmx;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function localName(name) {
|
|
759
|
+
return name.split(".").pop();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function callCached(cache, field, call) {
|
|
763
|
+
if (!cache[field]) {
|
|
764
|
+
cache[field] = call();
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
return await cache[field];
|
|
768
|
+
} catch (err) {
|
|
769
|
+
delete cache[field];
|
|
770
|
+
throw err;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function localEntityName(definition, req) {
|
|
775
|
+
const parts = definition.name.split(".");
|
|
776
|
+
const localName = [parts.pop()];
|
|
777
|
+
while (parts.length > 0) {
|
|
778
|
+
const context = req.csn.definitions[parts.join(".")];
|
|
779
|
+
if (context && context.kind === "entity") {
|
|
780
|
+
localName.unshift(parts.pop());
|
|
781
|
+
} else {
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const nameSuffix =
|
|
786
|
+
definition.kind === "entity" &&
|
|
787
|
+
definition.params &&
|
|
788
|
+
req.context.parameters &&
|
|
789
|
+
req.context.parameters.kind === "Set"
|
|
790
|
+
? "Set"
|
|
791
|
+
: "";
|
|
792
|
+
return localName.join("_") + nameSuffix;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function qualifiedName(name, req) {
|
|
796
|
+
const serviceNamespacePrefix = `${req.service}.`;
|
|
797
|
+
return (name.startsWith(serviceNamespacePrefix) ? "" : serviceNamespacePrefix) + name;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function lookupDefinition(name, req) {
|
|
801
|
+
const definitionName = qualifiedName(name, req);
|
|
802
|
+
return req.csn.definitions[definitionName] || req.csn.definitions[name];
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function lookupBoundDefinition(name, req) {
|
|
806
|
+
let boundAction = undefined;
|
|
807
|
+
structureKeys(req.csn.definitions).find((definitionName) => {
|
|
808
|
+
const definition = req.csn.definitions[definitionName];
|
|
809
|
+
return structureKeys(definition.actions).find((actionName) => {
|
|
810
|
+
if (name.endsWith(`_${actionName}`)) {
|
|
811
|
+
const entityName = name.substr(0, name.length - `_${actionName}`.length);
|
|
812
|
+
const entityDefinition = lookupDefinition(entityName, req);
|
|
813
|
+
if (entityDefinition === definition) {
|
|
814
|
+
boundAction = definition.actions[actionName];
|
|
815
|
+
req.lookupContext.boundDefinition = definition;
|
|
816
|
+
req.lookupContext.operation = boundAction;
|
|
817
|
+
const returnDefinition = lookupReturnDefinition(boundAction.returns, req);
|
|
818
|
+
if (returnDefinition) {
|
|
819
|
+
req.lookupContext.returnDefinition = returnDefinition;
|
|
820
|
+
}
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return false;
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
return boundAction;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function lookupParametersDefinition(name, req) {
|
|
831
|
+
const definitionTypeName = qualifiedName(name, req);
|
|
832
|
+
let definitionKind;
|
|
833
|
+
if (definitionTypeName.endsWith("Set")) {
|
|
834
|
+
definitionKind = "Set";
|
|
835
|
+
} else if (definitionTypeName.endsWith("Parameters")) {
|
|
836
|
+
definitionKind = "Parameters";
|
|
837
|
+
}
|
|
838
|
+
if (definitionKind) {
|
|
839
|
+
const definitionName = definitionTypeName.substring(0, definitionTypeName.length - definitionKind.length);
|
|
840
|
+
const definition = req.csn.definitions[definitionName] || req.csn.definitions[name];
|
|
841
|
+
if (definition && definition.kind === "entity" && definition.params) {
|
|
842
|
+
req.lookupContext.parameters = {
|
|
843
|
+
kind: definitionKind,
|
|
844
|
+
entity: localName(definitionName),
|
|
845
|
+
type: localName(definitionTypeName),
|
|
846
|
+
values: {},
|
|
847
|
+
keys: {},
|
|
848
|
+
count: false,
|
|
849
|
+
};
|
|
850
|
+
return definition;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function enhanceParametersDefinition(context, req) {
|
|
856
|
+
if (context && context.kind === "entity" && context.params) {
|
|
857
|
+
req.lookupContext.parameters = req.lookupContext.parameters || {
|
|
858
|
+
kind: "Parameters",
|
|
859
|
+
entity: localName(context.name),
|
|
860
|
+
type: localName(context.name),
|
|
861
|
+
values: {},
|
|
862
|
+
keys: {},
|
|
863
|
+
count: false,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Convert Proxy Request (v2 -> v4)
|
|
870
|
+
* @param proxyReq Proxy Request
|
|
871
|
+
* @param req Request
|
|
872
|
+
* @param res Response
|
|
873
|
+
*/
|
|
874
|
+
async function convertProxyRequest(proxyReq, req, res) {
|
|
875
|
+
try {
|
|
876
|
+
// Trace
|
|
877
|
+
traceRequest(req, "Request", req.method, req.originalUrl, req.headers, req.body);
|
|
878
|
+
|
|
879
|
+
const headers = propagateHeaders(req);
|
|
880
|
+
let body = req.body;
|
|
881
|
+
let contentType = req.header("content-type");
|
|
882
|
+
|
|
883
|
+
if (isMultipartMixed(contentType)) {
|
|
884
|
+
// Multipart
|
|
885
|
+
req.contentIdOrder = [];
|
|
886
|
+
body =
|
|
887
|
+
req.method === "HEAD"
|
|
888
|
+
? ""
|
|
889
|
+
: processMultipartMixed(
|
|
890
|
+
req,
|
|
891
|
+
req.body,
|
|
892
|
+
contentType,
|
|
893
|
+
({ method, url }) => {
|
|
894
|
+
return {
|
|
895
|
+
method: method === "MERGE" ? "PATCH" : method,
|
|
896
|
+
url: convertUrl(url, req),
|
|
897
|
+
};
|
|
898
|
+
},
|
|
899
|
+
({ contentType, body, headers, url, contentId }) => {
|
|
900
|
+
if (contentId) {
|
|
901
|
+
req.contentId[`$${contentId}`] = req.context.url;
|
|
902
|
+
}
|
|
903
|
+
delete headers.dataserviceversion;
|
|
904
|
+
delete headers.DataServiceVersion;
|
|
905
|
+
delete headers.maxdataserviceversion;
|
|
906
|
+
delete headers.MaxDataServiceVersion;
|
|
907
|
+
if (isApplicationJSON(contentType)) {
|
|
908
|
+
if (ieee754Compatible) {
|
|
909
|
+
contentType = enrichApplicationJSON(contentType);
|
|
910
|
+
headers["content-type"] = contentType;
|
|
911
|
+
}
|
|
912
|
+
body = convertRequestBody(body, headers, url, req);
|
|
913
|
+
}
|
|
914
|
+
return { body, headers };
|
|
915
|
+
},
|
|
916
|
+
req.contentIdOrder,
|
|
917
|
+
ProcessingDirection.Request
|
|
918
|
+
);
|
|
919
|
+
headers.accept = "multipart/mixed,application/json";
|
|
920
|
+
proxyReq.setHeader("accept", headers.accept);
|
|
921
|
+
} else {
|
|
922
|
+
// Single
|
|
923
|
+
proxyReq.path = convertUrl(proxyReq.path, req);
|
|
924
|
+
if (req.context.serviceRoot && (!headers.accept || headers.accept.includes("xml"))) {
|
|
925
|
+
req.context.serviceRootAsXML = true;
|
|
926
|
+
headers.accept = "application/json";
|
|
927
|
+
proxyReq.setHeader("accept", headers.accept);
|
|
928
|
+
} else if (headers.accept && !headers.accept.includes("application/json")) {
|
|
929
|
+
headers.accept = "application/json," + headers.accept;
|
|
930
|
+
proxyReq.setHeader("accept", headers.accept);
|
|
931
|
+
}
|
|
932
|
+
if (isApplicationJSON(contentType)) {
|
|
933
|
+
if (ieee754Compatible) {
|
|
934
|
+
contentType = enrichApplicationJSON(contentType);
|
|
935
|
+
headers["content-type"] = contentType;
|
|
936
|
+
}
|
|
937
|
+
body = convertRequestBody(req.body, req.headers, proxyReq.path, req);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
Object.entries(headers).forEach(([name, value]) => {
|
|
942
|
+
if (
|
|
943
|
+
name === "dataserviceversion" ||
|
|
944
|
+
name === "DataServiceVersion" ||
|
|
945
|
+
name === "maxdataserviceversion" ||
|
|
946
|
+
name === "MaxDataServiceVersion"
|
|
947
|
+
) {
|
|
948
|
+
delete headers[name];
|
|
949
|
+
proxyReq.removeHeader(name);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
if (continueOnError) {
|
|
954
|
+
headers["prefer"] = "odata.continue-on-error";
|
|
955
|
+
proxyReq.setHeader("prefer", "odata.continue-on-error");
|
|
956
|
+
}
|
|
957
|
+
headers["x-cds-odata-version"] = "v2";
|
|
958
|
+
proxyReq.setHeader("x-cds-odata-version", "v2");
|
|
959
|
+
|
|
960
|
+
if (req.body) {
|
|
961
|
+
delete req.body;
|
|
962
|
+
}
|
|
963
|
+
if (headers["x-http-method"]) {
|
|
964
|
+
proxyReq.method = headers["x-http-method"].toUpperCase();
|
|
965
|
+
}
|
|
966
|
+
proxyReq.method = proxyReq.method === "MERGE" ? "PATCH" : proxyReq.method;
|
|
967
|
+
|
|
968
|
+
if (contentType) {
|
|
969
|
+
if (body !== undefined) {
|
|
970
|
+
// File Upload
|
|
971
|
+
if (req.files && Object.keys(req.files).length === 1) {
|
|
972
|
+
const file = req.files[Object.keys(req.files)[0]];
|
|
973
|
+
contentType = body["content-type"] || file.mimetype;
|
|
974
|
+
body = file.data;
|
|
975
|
+
}
|
|
976
|
+
proxyReq.setHeader("content-type", contentType);
|
|
977
|
+
body = normalizeBody(body);
|
|
978
|
+
proxyReq.setHeader("content-length", Buffer.byteLength(body));
|
|
979
|
+
proxyReq.write(body);
|
|
980
|
+
proxyReq.end();
|
|
981
|
+
} else if ((req.header("transfer-encoding") || "").includes("chunked")) {
|
|
982
|
+
proxyReq.setHeader("content-type", contentType);
|
|
983
|
+
req.pipe(proxyReq);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Trace
|
|
988
|
+
traceRequest(req, "ProxyRequest", proxyReq.method, proxyReq.path, headers, body);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
// Error
|
|
991
|
+
logError(req, "Request", err);
|
|
992
|
+
if (err.statusCode) {
|
|
993
|
+
res.status(err.statusCode).send(err.message);
|
|
994
|
+
} else {
|
|
995
|
+
res.status(500).send("Internal Server Error");
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function convertUrl(urlPath, req) {
|
|
1001
|
+
let url = parseUrl(urlPath, req);
|
|
1002
|
+
const definition = lookupContextFromUrl(url, req);
|
|
1003
|
+
enrichRequest(definition, url, urlPath, req);
|
|
1004
|
+
|
|
1005
|
+
// Order is important
|
|
1006
|
+
convertUrlLinks(url, req);
|
|
1007
|
+
convertUrlDataTypes(url, req);
|
|
1008
|
+
convertUrlCount(url, req);
|
|
1009
|
+
convertDraft(url, req);
|
|
1010
|
+
convertActionFunction(url, req);
|
|
1011
|
+
convertFilter(url, req);
|
|
1012
|
+
convertExpandSelect(url, req);
|
|
1013
|
+
convertSearch(url, req);
|
|
1014
|
+
convertAnalytics(url, req);
|
|
1015
|
+
convertValue(url, req);
|
|
1016
|
+
convertParameters(url, req);
|
|
1017
|
+
|
|
1018
|
+
delete url.search;
|
|
1019
|
+
url.pathname = url.basePath + url.servicePath + url.contextPath;
|
|
1020
|
+
return URL.format(url);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function parseUrl(urlPath, req) {
|
|
1024
|
+
const url = URL.parse(urlPath, true);
|
|
1025
|
+
url.pathname = (url.pathname && url.pathname.replace(/%27/g, "'")) || "";
|
|
1026
|
+
url.originalUrl = { ...url, query: { ...url.query } };
|
|
1027
|
+
url.basePath = "";
|
|
1028
|
+
url.servicePath = "";
|
|
1029
|
+
url.contextPath = url.pathname;
|
|
1030
|
+
if (req.base && url.contextPath.startsWith(`/${req.base}`)) {
|
|
1031
|
+
url.basePath = `/${req.base}`;
|
|
1032
|
+
url.contextPath = url.contextPath.substr(url.basePath.length);
|
|
1033
|
+
}
|
|
1034
|
+
if (targetPath && url.contextPath.startsWith(targetPath)) {
|
|
1035
|
+
url.basePath = targetPath;
|
|
1036
|
+
url.contextPath = url.contextPath.substr(url.basePath.length);
|
|
1037
|
+
}
|
|
1038
|
+
if (url.contextPath.startsWith(`/${req.servicePath}`)) {
|
|
1039
|
+
url.servicePath = `/${req.servicePath}`;
|
|
1040
|
+
url.contextPath = url.contextPath.substr(url.servicePath.length);
|
|
1041
|
+
}
|
|
1042
|
+
if (url.contextPath.startsWith("/")) {
|
|
1043
|
+
url.servicePath += "/";
|
|
1044
|
+
url.contextPath = url.contextPath.substr(1);
|
|
1045
|
+
}
|
|
1046
|
+
url.originalUrl.servicePath = url.servicePath;
|
|
1047
|
+
url.originalUrl.contextPath = url.contextPath;
|
|
1048
|
+
// Normalize system and reserved query parameters (no array), others not (if array)
|
|
1049
|
+
Object.keys(url.query || {}).forEach((name) => {
|
|
1050
|
+
if (Array.isArray(url.query[name])) {
|
|
1051
|
+
if (name.startsWith("$") || ["search", "SideEffectsQualifier"].includes(name)) {
|
|
1052
|
+
url.query[name] = url.query[name][0];
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
return url;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function lookupContextFromUrl(url, req, context) {
|
|
1060
|
+
req.lookupContext = {};
|
|
1061
|
+
return contextFromUrl(url, req, context);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function contextFromUrl(url, req, context, suppressWarning) {
|
|
1065
|
+
let stop = false;
|
|
1066
|
+
return url.contextPath.split("/").reduce((context, part) => {
|
|
1067
|
+
if (stop) {
|
|
1068
|
+
return context;
|
|
1069
|
+
}
|
|
1070
|
+
const keyStart = part.indexOf("(");
|
|
1071
|
+
if (keyStart !== -1) {
|
|
1072
|
+
part = part.substr(0, keyStart);
|
|
1073
|
+
}
|
|
1074
|
+
context = lookupContext(part, context, req, suppressWarning);
|
|
1075
|
+
if (!context) {
|
|
1076
|
+
stop = true;
|
|
1077
|
+
}
|
|
1078
|
+
return context;
|
|
1079
|
+
}, context);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function lookupContext(name, context, req, suppressWarning) {
|
|
1083
|
+
if (!name) {
|
|
1084
|
+
return context;
|
|
1085
|
+
}
|
|
1086
|
+
if (!context) {
|
|
1087
|
+
if (name.startsWith("$") && req.contentId[name]) {
|
|
1088
|
+
return contextFromUrl(req.contentId[name], req, undefined, suppressWarning);
|
|
1089
|
+
} else {
|
|
1090
|
+
context = lookupDefinition(name, req);
|
|
1091
|
+
if (!context) {
|
|
1092
|
+
context = lookupBoundDefinition(name, req);
|
|
1093
|
+
}
|
|
1094
|
+
if (!context) {
|
|
1095
|
+
context = lookupParametersDefinition(name, req);
|
|
1096
|
+
}
|
|
1097
|
+
enhanceParametersDefinition(context, req);
|
|
1098
|
+
if (!context && !suppressWarning) {
|
|
1099
|
+
logWarning(req, "Context", "Definition name not found", {
|
|
1100
|
+
name,
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
if (context && (context.kind === "function" || context.kind === "action")) {
|
|
1104
|
+
req.lookupContext.operation = context;
|
|
1105
|
+
const returnDefinition = lookupReturnDefinition(context.returns, req);
|
|
1106
|
+
if (returnDefinition) {
|
|
1107
|
+
req.lookupContext.returnDefinition = returnDefinition;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return context;
|
|
1111
|
+
}
|
|
1112
|
+
} else {
|
|
1113
|
+
if (name.startsWith("$")) {
|
|
1114
|
+
return context;
|
|
1115
|
+
}
|
|
1116
|
+
if (context.kind === "function" || context.kind === "action") {
|
|
1117
|
+
req.lookupContext.operation = context;
|
|
1118
|
+
const returnDefinition = lookupReturnDefinition(context.returns, req);
|
|
1119
|
+
if (returnDefinition) {
|
|
1120
|
+
context = returnDefinition;
|
|
1121
|
+
req.lookupContext.returnDefinition = context;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const element = definitionElements(context)[name];
|
|
1125
|
+
if (element) {
|
|
1126
|
+
const type = elementType(element, req);
|
|
1127
|
+
if (type === "cds.Composition" || type === "cds.Association") {
|
|
1128
|
+
// Navigation
|
|
1129
|
+
return element._target;
|
|
1130
|
+
} else {
|
|
1131
|
+
// Element
|
|
1132
|
+
return context;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (context && context.kind === "entity" && context.params && ["Set", "Parameters"].includes(name)) {
|
|
1136
|
+
return context;
|
|
1137
|
+
}
|
|
1138
|
+
if (!suppressWarning) {
|
|
1139
|
+
logWarning(req, "Context", "Definition name not found", {
|
|
1140
|
+
name,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function enrichRequest(definition, url, urlPath, req) {
|
|
1147
|
+
req.context = {
|
|
1148
|
+
url,
|
|
1149
|
+
urlPath,
|
|
1150
|
+
serviceRoot: url.contextPath.length === 0,
|
|
1151
|
+
serviceRootAsXML: false,
|
|
1152
|
+
definition: definition,
|
|
1153
|
+
definitionElements: definitionElements(definition),
|
|
1154
|
+
requestDefinition: definition,
|
|
1155
|
+
serviceUri: "",
|
|
1156
|
+
operation: null,
|
|
1157
|
+
boundDefinition: null,
|
|
1158
|
+
returnDefinition: null,
|
|
1159
|
+
bodyParameters: {},
|
|
1160
|
+
$entityValue: false,
|
|
1161
|
+
$value: false,
|
|
1162
|
+
$count: false,
|
|
1163
|
+
$apply: null,
|
|
1164
|
+
aggregationKey: false,
|
|
1165
|
+
aggregationFilter: "",
|
|
1166
|
+
parameters: null,
|
|
1167
|
+
expandSiblingEntity: false,
|
|
1168
|
+
...req.lookupContext,
|
|
1169
|
+
};
|
|
1170
|
+
req.contexts.push(req.context);
|
|
1171
|
+
return req.context;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function convertUrlLinks(url, req) {
|
|
1175
|
+
url.contextPath = url.contextPath.replace(/\/\$links\//gi, "/");
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function convertUrlDataTypes(url, req) {
|
|
1179
|
+
// Keys & Parameters
|
|
1180
|
+
let context;
|
|
1181
|
+
let stop = false;
|
|
1182
|
+
url.contextPath = url.contextPath
|
|
1183
|
+
.split("/")
|
|
1184
|
+
.map((part) => {
|
|
1185
|
+
if (stop) {
|
|
1186
|
+
return part;
|
|
1187
|
+
}
|
|
1188
|
+
let keyPart = "";
|
|
1189
|
+
const keyStart = part.indexOf("(");
|
|
1190
|
+
const keyEnd = part.lastIndexOf(")");
|
|
1191
|
+
if (keyStart !== -1 && keyEnd === part.length - 1) {
|
|
1192
|
+
keyPart = part.substring(keyStart + 1, keyEnd);
|
|
1193
|
+
part = part.substr(0, keyStart);
|
|
1194
|
+
}
|
|
1195
|
+
context = lookupContext(part, context, req);
|
|
1196
|
+
if (!context) {
|
|
1197
|
+
stop = true;
|
|
1198
|
+
}
|
|
1199
|
+
const contextElements = definitionElements(context);
|
|
1200
|
+
const contextKeys = definitionKeys(context);
|
|
1201
|
+
if (context && keyPart) {
|
|
1202
|
+
const aggregationMatch =
|
|
1203
|
+
keyPart.match(/^aggregation'(.*)'$/is) ||
|
|
1204
|
+
keyPart.match(/^'aggregation'(.*)''$/is) ||
|
|
1205
|
+
keyPart.match(/^ID__='aggregation'(.*)''$/is);
|
|
1206
|
+
const aggregationKey = aggregationMatch && aggregationMatch.pop();
|
|
1207
|
+
if (aggregationKey) {
|
|
1208
|
+
// Aggregation Key
|
|
1209
|
+
try {
|
|
1210
|
+
const aggregation = JSON.parse(decodeURIKey(aggregationKey));
|
|
1211
|
+
url.query["$select"] = (aggregation.value || []).join(",");
|
|
1212
|
+
delete url.query["$filter"];
|
|
1213
|
+
if (Object.keys(aggregation.key || {}).length > 0) {
|
|
1214
|
+
url.query["$filter"] = Object.keys(aggregation.key || {})
|
|
1215
|
+
.map((name) => {
|
|
1216
|
+
return `${name} eq ${aggregation.key[name]}`;
|
|
1217
|
+
})
|
|
1218
|
+
.join(" and ");
|
|
1219
|
+
}
|
|
1220
|
+
if (aggregation.filter) {
|
|
1221
|
+
if (!url.query["$filter"]) {
|
|
1222
|
+
url.query["$filter"] = aggregation.filter;
|
|
1223
|
+
} else {
|
|
1224
|
+
url.query["$filter"] = `(${url.query["$filter"]}) and (${aggregation.filter})`;
|
|
1225
|
+
}
|
|
1226
|
+
req.context.aggregationFilter = aggregation.filter;
|
|
1227
|
+
}
|
|
1228
|
+
if (aggregation.search) {
|
|
1229
|
+
url.query["search"] = aggregation.search;
|
|
1230
|
+
req.context.aggregationSearch = aggregation.search;
|
|
1231
|
+
}
|
|
1232
|
+
req.context.aggregationKey = true;
|
|
1233
|
+
return part;
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
// Error
|
|
1236
|
+
logError(req, "AggregationKey", err);
|
|
1237
|
+
return part;
|
|
1238
|
+
}
|
|
1239
|
+
} else {
|
|
1240
|
+
const keys = decodeURIComponent(keyPart).split(",");
|
|
1241
|
+
return encodeURIComponent(
|
|
1242
|
+
`${part}(${keys
|
|
1243
|
+
.map((key) => {
|
|
1244
|
+
const [name, value] = key.split("=");
|
|
1245
|
+
let type;
|
|
1246
|
+
if (name && value) {
|
|
1247
|
+
if (context.params && context.params[name]) {
|
|
1248
|
+
type = context.params[name].type;
|
|
1249
|
+
}
|
|
1250
|
+
if (!type) {
|
|
1251
|
+
type = elementType(contextElements[name], req);
|
|
1252
|
+
}
|
|
1253
|
+
return `${name}=${replaceConvertDataTypeToV4(value, type)}`;
|
|
1254
|
+
} else if (name) {
|
|
1255
|
+
const key = structureKeys(contextKeys).find((key) => {
|
|
1256
|
+
return contextKeys[key].type !== "cds.Composition" && contextKeys[key].type !== "cds.Association";
|
|
1257
|
+
});
|
|
1258
|
+
type = key && elementType(contextElements[key], req);
|
|
1259
|
+
return type && `${replaceConvertDataTypeToV4(name, type)}`;
|
|
1260
|
+
}
|
|
1261
|
+
return "";
|
|
1262
|
+
})
|
|
1263
|
+
.filter((part) => !!part)
|
|
1264
|
+
.join(",")})`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
} else {
|
|
1268
|
+
return part;
|
|
1269
|
+
}
|
|
1270
|
+
})
|
|
1271
|
+
.join("/");
|
|
1272
|
+
|
|
1273
|
+
// Query
|
|
1274
|
+
Object.keys(url.query).forEach((name) => {
|
|
1275
|
+
if (name === "$filter") {
|
|
1276
|
+
url.query[name] = convertUrlDataTypesForFilter(url.query[name], context, req);
|
|
1277
|
+
} else if (!name.startsWith("$")) {
|
|
1278
|
+
const contextElements = definitionElements(context);
|
|
1279
|
+
if (contextElements[name]) {
|
|
1280
|
+
const element = contextElements[name];
|
|
1281
|
+
const type = elementType(element, req);
|
|
1282
|
+
if (DataTypeMap[type]) {
|
|
1283
|
+
url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (context && (context.kind === "function" || context.kind === "action")) {
|
|
1287
|
+
if (context.params && context.params[name]) {
|
|
1288
|
+
const element = context.params[name];
|
|
1289
|
+
const type = elementType(element, req);
|
|
1290
|
+
if (DataTypeMap[type]) {
|
|
1291
|
+
url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (context.parent && context.parent.kind === "entity") {
|
|
1295
|
+
const parentElements = definitionElements(context.parent);
|
|
1296
|
+
if (parentElements[name]) {
|
|
1297
|
+
const element = parentElements[name];
|
|
1298
|
+
const type = elementType(element, req);
|
|
1299
|
+
if (DataTypeMap[type]) {
|
|
1300
|
+
url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function buildQuoteParts(input) {
|
|
1310
|
+
let quote = false;
|
|
1311
|
+
let quoteEscape = false;
|
|
1312
|
+
let quoteTypeStart = false;
|
|
1313
|
+
let part = "";
|
|
1314
|
+
const parts = [];
|
|
1315
|
+
input.split("").forEach((char, index) => {
|
|
1316
|
+
part += char;
|
|
1317
|
+
if (char === "'") {
|
|
1318
|
+
if (quote) {
|
|
1319
|
+
if (quoteEscape) {
|
|
1320
|
+
quoteEscape = false;
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const nextChar = input.substr(index + 1, 1);
|
|
1324
|
+
if (nextChar === "'") {
|
|
1325
|
+
quoteEscape = true;
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const typeStart = !!Object.keys(DataTypeMap)
|
|
1330
|
+
.filter((type) => !["cds.String", "cds.LargeString"].includes(type))
|
|
1331
|
+
.find((type) => {
|
|
1332
|
+
const v2Pattern = DataTypeMap[type].v2;
|
|
1333
|
+
return v2Pattern.includes("'") && part.endsWith(v2Pattern.split("'").shift() + "'");
|
|
1334
|
+
});
|
|
1335
|
+
if (!typeStart && !quoteTypeStart) {
|
|
1336
|
+
if (part.length > 0) {
|
|
1337
|
+
parts.push({
|
|
1338
|
+
content: part,
|
|
1339
|
+
quote: quote,
|
|
1340
|
+
});
|
|
1341
|
+
part = "";
|
|
1342
|
+
}
|
|
1343
|
+
quote = !quote;
|
|
1344
|
+
}
|
|
1345
|
+
quoteTypeStart = typeStart;
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
if (part.length > 0) {
|
|
1349
|
+
parts.push({
|
|
1350
|
+
content: part,
|
|
1351
|
+
quote: quote,
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
return parts;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function convertUrlDataTypesForFilter(filter, context, req) {
|
|
1358
|
+
if (filter === null || filter === undefined) {
|
|
1359
|
+
return filter;
|
|
1360
|
+
}
|
|
1361
|
+
return buildQuoteParts(filter)
|
|
1362
|
+
.map((part) => {
|
|
1363
|
+
if (!part.quote) {
|
|
1364
|
+
convertUrlDataTypesForFilterElements(part, context, req);
|
|
1365
|
+
}
|
|
1366
|
+
return part.content;
|
|
1367
|
+
})
|
|
1368
|
+
.join("");
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function convertUrlDataTypesForFilterElements(part, entity, req, path = "", depth = 0) {
|
|
1372
|
+
const elements = definitionElements(entity);
|
|
1373
|
+
for (let name of structureKeys(elements)) {
|
|
1374
|
+
const namePath = (path ? `${path}/` : "") + name;
|
|
1375
|
+
if (part.content.includes(namePath)) {
|
|
1376
|
+
const element = elements[name];
|
|
1377
|
+
const type = elementType(element, req);
|
|
1378
|
+
if (type !== "cds.Composition" && type !== "cds.Association") {
|
|
1379
|
+
if (DataTypeMap[type]) {
|
|
1380
|
+
const v4Regex = new RegExp(
|
|
1381
|
+
`(${namePath})(\\)?\\s+?(?:eq|ne|gt|ge|lt|le)\\s+?)${DataTypeMap[type].v4.source}`,
|
|
1382
|
+
DataTypeMap[type].v4.flags
|
|
1383
|
+
);
|
|
1384
|
+
if (v4Regex.test(part.content)) {
|
|
1385
|
+
part.content = part.content.replace(v4Regex, (_, name, op, value) => {
|
|
1386
|
+
return `${name}${op}${convertDataTypeToV4(value, type)}`;
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
} else if (depth < 3 && (!element.cardinality || element.cardinality.max !== "*")) {
|
|
1391
|
+
convertUrlDataTypesForFilterElements(part, element._target, req, namePath, depth + 1);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function convertUrlCount(url, req) {
|
|
1398
|
+
if (url.query["$inlinecount"]) {
|
|
1399
|
+
url.query["$count"] = url.query["$inlinecount"] === "allpages";
|
|
1400
|
+
req.context.$count = url.query["$count"];
|
|
1401
|
+
delete url.query["$inlinecount"];
|
|
1402
|
+
}
|
|
1403
|
+
return url;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function convertDraft(url, req) {
|
|
1407
|
+
if (
|
|
1408
|
+
req.context &&
|
|
1409
|
+
req.context.definition &&
|
|
1410
|
+
req.context.definition.kind === "action" &&
|
|
1411
|
+
req.context.definition.params &&
|
|
1412
|
+
req.context.definition.params.SideEffectsQualifier
|
|
1413
|
+
) {
|
|
1414
|
+
url.query.SideEffectsQualifier = url.query.SideEffectsQualifier || "";
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function convertActionFunction(url, req) {
|
|
1419
|
+
const definition = req.context && (req.context.operation || req.context.definition);
|
|
1420
|
+
if (!(definition && (definition.kind === "function" || definition.kind === "action"))) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const operationLocalName = localEntityName(definition, req);
|
|
1424
|
+
let reqContextPathSuffix = "";
|
|
1425
|
+
if (url.contextPath.startsWith(operationLocalName)) {
|
|
1426
|
+
reqContextPathSuffix = url.contextPath.substr(operationLocalName.length);
|
|
1427
|
+
url.contextPath = url.contextPath.substr(0, operationLocalName.length);
|
|
1428
|
+
}
|
|
1429
|
+
// Key Parameters
|
|
1430
|
+
if (definition.parent && definition.parent.kind === "entity") {
|
|
1431
|
+
url.contextPath = localEntityName(definition.parent, req);
|
|
1432
|
+
url.contextPath += `(${structureKeys(definitionKeys(definition.parent))
|
|
1433
|
+
.reduce((result, name) => {
|
|
1434
|
+
const parentElements = definitionElements(definition.parent);
|
|
1435
|
+
const element = parentElements[name];
|
|
1436
|
+
const type = elementType(element, req);
|
|
1437
|
+
if (!(type === "cds.Composition" || type === "cds.Association")) {
|
|
1438
|
+
const value = url.query[name];
|
|
1439
|
+
result.push(`${name}=${quoteParameter(element, value, req)}`);
|
|
1440
|
+
delete url.query[name];
|
|
1441
|
+
}
|
|
1442
|
+
return result;
|
|
1443
|
+
}, [])
|
|
1444
|
+
.join(",")})`;
|
|
1445
|
+
url.contextPath += `/${req.service}.${definition.name}`;
|
|
1446
|
+
}
|
|
1447
|
+
// Function Parameters
|
|
1448
|
+
if (definition.kind === "function") {
|
|
1449
|
+
url.contextPath += `(${Object.keys(url.query)
|
|
1450
|
+
.reduce((result, name) => {
|
|
1451
|
+
if (!name.startsWith("$")) {
|
|
1452
|
+
const element = definition.params && definition.params[name];
|
|
1453
|
+
if (element) {
|
|
1454
|
+
let value = url.query[name];
|
|
1455
|
+
if (Array.isArray(value)) {
|
|
1456
|
+
value = value.map((entry) => {
|
|
1457
|
+
return quoteParameter(element, encodeURIComponent(entry), req);
|
|
1458
|
+
});
|
|
1459
|
+
} else {
|
|
1460
|
+
value = quoteParameter(element, encodeURIComponent(value), req);
|
|
1461
|
+
if (element.items && element.items.type) {
|
|
1462
|
+
value = [value];
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (Array.isArray(value)) {
|
|
1466
|
+
result.push(`${name}=@${name}Col`);
|
|
1467
|
+
value = value.map((entry) => {
|
|
1468
|
+
return quoteParameter(element, entry, req, '"');
|
|
1469
|
+
});
|
|
1470
|
+
url.query[`@${name}Col`] = `[${value}]`;
|
|
1471
|
+
} else {
|
|
1472
|
+
result.push(`${name}=${value}`);
|
|
1473
|
+
}
|
|
1474
|
+
delete url.query[name];
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return result;
|
|
1478
|
+
}, [])
|
|
1479
|
+
.join(",")})`;
|
|
1480
|
+
}
|
|
1481
|
+
url.contextPath += reqContextPathSuffix;
|
|
1482
|
+
// Action Body
|
|
1483
|
+
if (definition.kind === "action") {
|
|
1484
|
+
Object.keys(url.query).forEach((name) => {
|
|
1485
|
+
if (!name.startsWith("$")) {
|
|
1486
|
+
const element = definition.params && definition.params[name];
|
|
1487
|
+
if (element) {
|
|
1488
|
+
let value = url.query[name];
|
|
1489
|
+
if (Array.isArray(value)) {
|
|
1490
|
+
value = value.map((entry) => {
|
|
1491
|
+
return unescapeSingleQuote(element, unquoteParameter(element, entry, req), req);
|
|
1492
|
+
});
|
|
1493
|
+
} else {
|
|
1494
|
+
value = unescapeSingleQuote(element, unquoteParameter(element, value, req), req);
|
|
1495
|
+
if (element.items && element.items.type) {
|
|
1496
|
+
value = [value];
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
req.context.bodyParameters[name] = value;
|
|
1500
|
+
delete url.query[name];
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function unescapeSingleQuote(element, value, req) {
|
|
1508
|
+
if (element && value && ["cds.String", "cds.LargeString"].includes(elementType(element, req))) {
|
|
1509
|
+
return value.replace(/''/g, "'");
|
|
1510
|
+
}
|
|
1511
|
+
return value;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function quoteParameter(element, value, req, quote = "'") {
|
|
1515
|
+
if (element && ["cds.String", "cds.LargeString"].includes(elementType(element, req))) {
|
|
1516
|
+
return `${quote}${unquoteParameter(element, value, req)}${quote}`;
|
|
1517
|
+
}
|
|
1518
|
+
return value;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function unquoteParameter(element, value, req) {
|
|
1522
|
+
if (
|
|
1523
|
+
element &&
|
|
1524
|
+
value &&
|
|
1525
|
+
[
|
|
1526
|
+
"cds.String",
|
|
1527
|
+
"cds.LargeString",
|
|
1528
|
+
"cds.UUID",
|
|
1529
|
+
"cds.Binary",
|
|
1530
|
+
"cds.LargeBinary",
|
|
1531
|
+
"cds.Date",
|
|
1532
|
+
"cds.Time",
|
|
1533
|
+
"cds.DateTime",
|
|
1534
|
+
"cds.Timestamp",
|
|
1535
|
+
].includes(elementType(element, req))
|
|
1536
|
+
) {
|
|
1537
|
+
return value.replace(/^['](.*)[']$/s, "$1");
|
|
1538
|
+
}
|
|
1539
|
+
return value;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function stripSlashes(path) {
|
|
1543
|
+
return path && path.replace(/^\/|\/$/g, "");
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function normalizeSlashes(path) {
|
|
1547
|
+
path = stripSlashes(path);
|
|
1548
|
+
return path ? `/${path}/` : "/";
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function convertExpandSelect(url, req) {
|
|
1552
|
+
const definition = req.context && req.context.definition;
|
|
1553
|
+
if (definition) {
|
|
1554
|
+
const context = { select: {}, expand: {} };
|
|
1555
|
+
if (url.query["$expand"]) {
|
|
1556
|
+
let expands = url.query["$expand"].split(",");
|
|
1557
|
+
if (definition.kind === "entity" && definition.params) {
|
|
1558
|
+
expands = expands.filter((expand) => !["Set", "Parameters"].includes(expand));
|
|
1559
|
+
}
|
|
1560
|
+
expands.forEach((expand) => {
|
|
1561
|
+
if (fixDraftRequests && expand === "SiblingEntity") {
|
|
1562
|
+
req.context.expandSiblingEntity = true;
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
let current = context.expand;
|
|
1566
|
+
expand.split("/").forEach((part) => {
|
|
1567
|
+
current[part] = current[part] || { select: {}, expand: {} };
|
|
1568
|
+
current = current[part].expand;
|
|
1569
|
+
});
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
if (url.query["$select"]) {
|
|
1573
|
+
const selects = url.query["$select"].split(",");
|
|
1574
|
+
selects.forEach((select) => {
|
|
1575
|
+
let current = context;
|
|
1576
|
+
let currentDefinition = definition;
|
|
1577
|
+
select.split("/").forEach((part) => {
|
|
1578
|
+
if (!current) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
const element = definitionElements(currentDefinition)[part];
|
|
1582
|
+
if (element) {
|
|
1583
|
+
const type = elementType(element, req);
|
|
1584
|
+
if (type === "cds.Composition" || type === "cds.Association") {
|
|
1585
|
+
current = current && current.expand[part];
|
|
1586
|
+
currentDefinition = element._target;
|
|
1587
|
+
} else if (current && current.select) {
|
|
1588
|
+
current.select[part] = true;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
if (Object.keys(context.select).length > 0) {
|
|
1594
|
+
url.query["$select"] = Object.keys(context.select).join(",");
|
|
1595
|
+
} else {
|
|
1596
|
+
delete url.query["$select"];
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (url.query["$expand"]) {
|
|
1600
|
+
const serializeExpand = (expand) => {
|
|
1601
|
+
return Object.keys(expand || {})
|
|
1602
|
+
.map((name) => {
|
|
1603
|
+
let value = expand[name];
|
|
1604
|
+
let result = name;
|
|
1605
|
+
const selects = Object.keys(value.select);
|
|
1606
|
+
const expands = Object.keys(value.expand);
|
|
1607
|
+
if (selects.length > 0 || expands.length > 0) {
|
|
1608
|
+
result += "(";
|
|
1609
|
+
if (selects.length > 0) {
|
|
1610
|
+
result += `$select=${selects.join(",")}`;
|
|
1611
|
+
}
|
|
1612
|
+
if (expands.length > 0) {
|
|
1613
|
+
if (selects.length > 0) {
|
|
1614
|
+
result += ";";
|
|
1615
|
+
}
|
|
1616
|
+
result += `$expand=${serializeExpand(value.expand)}`;
|
|
1617
|
+
}
|
|
1618
|
+
result += ")";
|
|
1619
|
+
}
|
|
1620
|
+
return result;
|
|
1621
|
+
})
|
|
1622
|
+
.join(",");
|
|
1623
|
+
};
|
|
1624
|
+
if (Object.keys(context.expand).length > 0) {
|
|
1625
|
+
url.query["$expand"] = serializeExpand(context.expand);
|
|
1626
|
+
} else {
|
|
1627
|
+
delete url.query["$expand"];
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function convertFilter(url, req) {
|
|
1634
|
+
const _ = "§§";
|
|
1635
|
+
|
|
1636
|
+
let filter = url.query["$filter"];
|
|
1637
|
+
if (filter) {
|
|
1638
|
+
// Fix unsupported draft requests
|
|
1639
|
+
if (fixDraftRequests) {
|
|
1640
|
+
const match = filter.match(UnsupportedDraftFilterRegex);
|
|
1641
|
+
if (match && match.length === 3 && match[1] === match[2]) {
|
|
1642
|
+
filter = filter.replace(match[0], match[1]);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
// Convert functions
|
|
1646
|
+
let quote = false;
|
|
1647
|
+
let lookBehind = "";
|
|
1648
|
+
let bracket = 0;
|
|
1649
|
+
let brackets = [];
|
|
1650
|
+
let bracketMax = 0;
|
|
1651
|
+
|
|
1652
|
+
filter = filter
|
|
1653
|
+
.split("")
|
|
1654
|
+
.map((char) => {
|
|
1655
|
+
if (char === "'") {
|
|
1656
|
+
quote = !quote;
|
|
1657
|
+
}
|
|
1658
|
+
if (!quote) {
|
|
1659
|
+
if (char === "(") {
|
|
1660
|
+
bracket++;
|
|
1661
|
+
const filterFunctionStart = !!Object.keys(FilterFunctions).find((name) => {
|
|
1662
|
+
return lookBehind.endsWith(name.split("(").shift());
|
|
1663
|
+
});
|
|
1664
|
+
if (filterFunctionStart) {
|
|
1665
|
+
brackets.push(bracket - 1);
|
|
1666
|
+
bracketMax = Math.max(bracketMax, brackets.length);
|
|
1667
|
+
return `${_}(${brackets.length}${_}`;
|
|
1668
|
+
}
|
|
1669
|
+
} else if (char === ")") {
|
|
1670
|
+
bracket--;
|
|
1671
|
+
const [mark] = brackets.slice(-1);
|
|
1672
|
+
if (mark === bracket) {
|
|
1673
|
+
brackets.pop();
|
|
1674
|
+
return `${_})${brackets.length + 1}${_}`;
|
|
1675
|
+
}
|
|
1676
|
+
} else if (char === ",") {
|
|
1677
|
+
if (brackets.length > 0) {
|
|
1678
|
+
return `${_},${brackets.length}${_}`;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
lookBehind += char;
|
|
1682
|
+
} else {
|
|
1683
|
+
lookBehind = "";
|
|
1684
|
+
}
|
|
1685
|
+
return char;
|
|
1686
|
+
})
|
|
1687
|
+
.join("");
|
|
1688
|
+
|
|
1689
|
+
if (bracketMax > 0) {
|
|
1690
|
+
for (let i = 1; i <= bracketMax; i++) {
|
|
1691
|
+
Object.keys(FilterFunctions).forEach((name) => {
|
|
1692
|
+
let pattern = name
|
|
1693
|
+
.replace(/([()])/g, `${_}\\$1${i}${_}`)
|
|
1694
|
+
.replace(/([,])/g, `${_}$1${i}${_}`)
|
|
1695
|
+
.replace(/[$]/g, "(.*?)");
|
|
1696
|
+
filter = filter.replace(new RegExp(pattern, "gi"), FilterFunctions[name]);
|
|
1697
|
+
});
|
|
1698
|
+
filter = filter.replace(new RegExp(`${_}([(),])${i}${_}`, "g"), "$1");
|
|
1699
|
+
}
|
|
1700
|
+
url.query["$filter"] = filter;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function convertSearch(url, req) {
|
|
1706
|
+
if (url.query.search) {
|
|
1707
|
+
let search = url.query.search;
|
|
1708
|
+
if (quoteSearch) {
|
|
1709
|
+
search = `"${search.replace(/"/g, `\\"`)}"`;
|
|
1710
|
+
} else {
|
|
1711
|
+
if (!/^".*"$/s.test(search) && search.includes('"')) {
|
|
1712
|
+
search = `"${search.replace(/"/g, `\\"`)}"`;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
url.query["$search"] = search;
|
|
1716
|
+
delete url.query.search;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function convertAnalytics(url, req) {
|
|
1721
|
+
const definition = req.context && req.context.definition;
|
|
1722
|
+
if (
|
|
1723
|
+
!(
|
|
1724
|
+
definition &&
|
|
1725
|
+
definition.kind === "entity" &&
|
|
1726
|
+
definition["@cov2ap.analytics"] !== false &&
|
|
1727
|
+
url.query["$select"] &&
|
|
1728
|
+
(definition["@cov2ap.analytics"] === true ||
|
|
1729
|
+
definition["@Analytics"] ||
|
|
1730
|
+
definition["@Analytics.AnalyticalContext"] ||
|
|
1731
|
+
definition["@Analytics.query"] ||
|
|
1732
|
+
definition["@AnalyticalContext"] ||
|
|
1733
|
+
definition["@Aggregation.ApplySupported.PropertyRestrictions"] ||
|
|
1734
|
+
definition["@sap.semantics"] === "aggregate")
|
|
1735
|
+
)
|
|
1736
|
+
) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const elements = req.context.definitionElements;
|
|
1740
|
+
const measures = [];
|
|
1741
|
+
const dimensions = [];
|
|
1742
|
+
const selects = url.query["$select"].split(",");
|
|
1743
|
+
const values = [];
|
|
1744
|
+
selects.forEach((select) => {
|
|
1745
|
+
const element = elements[select];
|
|
1746
|
+
if (element) {
|
|
1747
|
+
values.push(element);
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
selects.forEach((select) => {
|
|
1751
|
+
const element = elements[select];
|
|
1752
|
+
if (element) {
|
|
1753
|
+
if (
|
|
1754
|
+
element["@Analytics.AnalyticalContext.Measure"] ||
|
|
1755
|
+
element["@AnalyticalContext.Measure"] ||
|
|
1756
|
+
element["@Analytics.Measure"] ||
|
|
1757
|
+
element["@sap.aggregation.role"] === "measure"
|
|
1758
|
+
) {
|
|
1759
|
+
measures.push(element);
|
|
1760
|
+
} else {
|
|
1761
|
+
// element["@Analytics.AnalyticalContext.Dimension"] || element["@AnalyticalContext.Dimension"] || element["@Analytics.Dimension"] || element["@sap.aggregation.role"] === "dimension"
|
|
1762
|
+
dimensions.push(element);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
if (dimensions.length > 0 || measures.length > 0) {
|
|
1768
|
+
url.query["$apply"] = "";
|
|
1769
|
+
if (dimensions.length) {
|
|
1770
|
+
url.query["$apply"] = "groupby(";
|
|
1771
|
+
url.query["$apply"] += `(${dimensions
|
|
1772
|
+
.map((dimension) => {
|
|
1773
|
+
return dimension.name;
|
|
1774
|
+
})
|
|
1775
|
+
.join(",")})`;
|
|
1776
|
+
}
|
|
1777
|
+
if (measures.length > 0) {
|
|
1778
|
+
if (url.query["$apply"]) {
|
|
1779
|
+
url.query["$apply"] += ",";
|
|
1780
|
+
}
|
|
1781
|
+
url.query["$apply"] += `aggregate(${measures
|
|
1782
|
+
.map((measure) => {
|
|
1783
|
+
const aggregation = measure["@Aggregation.default"] || measure["@DefaultAggregation"];
|
|
1784
|
+
const aggregationName = aggregation ? aggregation["#"] || aggregation : DefaultAggregation;
|
|
1785
|
+
const aggregationFunction = aggregationName ? AggregationMap[aggregationName.toUpperCase()] : undefined;
|
|
1786
|
+
if (!aggregationFunction) {
|
|
1787
|
+
throw new Error(`Aggregation '${aggregationName}' is not supported`);
|
|
1788
|
+
}
|
|
1789
|
+
if ([AggregationMap.NONE, AggregationMap.NOP].includes(aggregationFunction)) {
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
if (aggregationFunction.startsWith("$")) {
|
|
1793
|
+
return `${aggregationFunction} as ${AggregationPrefix}${measure.name}`;
|
|
1794
|
+
} else {
|
|
1795
|
+
return `${measure.name} with ${aggregationFunction} as ${AggregationPrefix}${measure.name}`;
|
|
1796
|
+
}
|
|
1797
|
+
})
|
|
1798
|
+
.filter((aggregation) => !!aggregation)
|
|
1799
|
+
.join(",")})`;
|
|
1800
|
+
}
|
|
1801
|
+
if (dimensions.length) {
|
|
1802
|
+
url.query["$apply"] += ")";
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const filter = url.query["$filter"];
|
|
1806
|
+
if (filter) {
|
|
1807
|
+
url.query["$apply"] = `filter(${filter})/` + url.query["$apply"];
|
|
1808
|
+
}
|
|
1809
|
+
const search = url.query["$search"];
|
|
1810
|
+
if (search) {
|
|
1811
|
+
url.query["$apply"] = `search(${search})/` + url.query["$apply"];
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
if (url.query["$orderby"]) {
|
|
1815
|
+
url.query["$orderby"] = url.query["$orderby"]
|
|
1816
|
+
.split(",")
|
|
1817
|
+
.map((orderBy) => {
|
|
1818
|
+
let [name, order] = orderBy.split(" ");
|
|
1819
|
+
const element = elements[name];
|
|
1820
|
+
if (
|
|
1821
|
+
element &&
|
|
1822
|
+
(element["@Analytics.AnalyticalContext.Measure"] ||
|
|
1823
|
+
element["@AnalyticalContext.Measure"] ||
|
|
1824
|
+
element["@Analytics.Measure"] ||
|
|
1825
|
+
element["@sap.aggregation.role"] === "measure")
|
|
1826
|
+
) {
|
|
1827
|
+
name = `${AggregationPrefix}${element.name}`;
|
|
1828
|
+
}
|
|
1829
|
+
return name + (order ? ` ${order}` : "");
|
|
1830
|
+
})
|
|
1831
|
+
.join(",");
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
delete url.query["$filter"];
|
|
1835
|
+
delete url.query["$select"];
|
|
1836
|
+
delete url.query["$expand"];
|
|
1837
|
+
delete url.query["$search"];
|
|
1838
|
+
|
|
1839
|
+
req.context.$apply = {
|
|
1840
|
+
key: dimensions,
|
|
1841
|
+
value: values,
|
|
1842
|
+
filter: req.context.aggregationKey ? req.context.aggregationFilter : filter,
|
|
1843
|
+
search: req.context.aggregationKey ? req.context.aggregationSearch : search,
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function convertValue(url, req) {
|
|
1849
|
+
if (url.contextPath.endsWith("/$value")) {
|
|
1850
|
+
url.contextPath = url.contextPath.substr(0, url.contextPath.length - "/$value".length);
|
|
1851
|
+
const mediaDataElementName =
|
|
1852
|
+
req.context &&
|
|
1853
|
+
req.context.definition &&
|
|
1854
|
+
findElementByAnnotation(req.context.definitionElements, "@Core.MediaType");
|
|
1855
|
+
const endingElementName = findEndingElementName(req.context.definitionElements, url);
|
|
1856
|
+
if (!endingElementName) {
|
|
1857
|
+
url.contextPath += `/${mediaDataElementName}`;
|
|
1858
|
+
req.context.$entityValue = true;
|
|
1859
|
+
} else if (endingElementName !== mediaDataElementName) {
|
|
1860
|
+
req.context.$value = true;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function convertParameters(url, req) {
|
|
1866
|
+
if (req.context.parameters) {
|
|
1867
|
+
let context;
|
|
1868
|
+
let stop = false;
|
|
1869
|
+
url.contextPath = url.contextPath
|
|
1870
|
+
.split("/")
|
|
1871
|
+
.map((part) => {
|
|
1872
|
+
if (part === "Set" || part.startsWith("Set(")) {
|
|
1873
|
+
req.context.parameters.kind = "Set";
|
|
1874
|
+
stop = true;
|
|
1875
|
+
} else if (part === "Parameters" || part.startsWith("Parameters(")) {
|
|
1876
|
+
req.context.parameters.kind = "Parameters";
|
|
1877
|
+
stop = true;
|
|
1878
|
+
} else if (part === "$count") {
|
|
1879
|
+
req.context.parameters.count = true;
|
|
1880
|
+
}
|
|
1881
|
+
if (stop) {
|
|
1882
|
+
return "";
|
|
1883
|
+
}
|
|
1884
|
+
let keyPart = "";
|
|
1885
|
+
const keyStart = part.indexOf("(");
|
|
1886
|
+
const keyEnd = part.lastIndexOf(")");
|
|
1887
|
+
if (keyStart !== -1 && keyEnd === part.length - 1) {
|
|
1888
|
+
keyPart = part.substring(keyStart + 1, keyEnd);
|
|
1889
|
+
part = part.substr(0, keyStart);
|
|
1890
|
+
}
|
|
1891
|
+
if (part === req.context.parameters.type) {
|
|
1892
|
+
part = req.context.parameters.entity;
|
|
1893
|
+
}
|
|
1894
|
+
context = lookupContext(part, context, req);
|
|
1895
|
+
if (!context) {
|
|
1896
|
+
stop = true;
|
|
1897
|
+
}
|
|
1898
|
+
const contextElements = definitionElements(context);
|
|
1899
|
+
if (context && keyPart) {
|
|
1900
|
+
const keys = decodeURIComponent(keyPart).split(",");
|
|
1901
|
+
return encodeURIComponent(
|
|
1902
|
+
`${part}(${keys
|
|
1903
|
+
.map((key) => {
|
|
1904
|
+
const [name, value] = key.split("=");
|
|
1905
|
+
if (name && value) {
|
|
1906
|
+
if (context.params[name]) {
|
|
1907
|
+
req.context.parameters.values[name] = unquoteParameter(context.params[name], value, req);
|
|
1908
|
+
return `${name}=${value}`;
|
|
1909
|
+
} else {
|
|
1910
|
+
req.context.parameters.keys[name] = unquoteParameter(contextElements[name], value, req);
|
|
1911
|
+
}
|
|
1912
|
+
} else if (name) {
|
|
1913
|
+
const param = structureKeys(context.params).find(() => true);
|
|
1914
|
+
if (param) {
|
|
1915
|
+
if (context.params[param]) {
|
|
1916
|
+
req.context.parameters.values[param] = unquoteParameter(context.params[param], name, req);
|
|
1917
|
+
return `${param}=${name}`;
|
|
1918
|
+
} else {
|
|
1919
|
+
req.context.parameters.keys[param] = unquoteParameter(contextElements[param], name, req);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return "";
|
|
1924
|
+
})
|
|
1925
|
+
.filter((part) => !!part)
|
|
1926
|
+
.join(",")})`
|
|
1927
|
+
);
|
|
1928
|
+
} else {
|
|
1929
|
+
return part;
|
|
1930
|
+
}
|
|
1931
|
+
})
|
|
1932
|
+
.filter((part) => !!part)
|
|
1933
|
+
.join("/");
|
|
1934
|
+
if (!url.contextPath.endsWith("/Set")) {
|
|
1935
|
+
url.contextPath = `${url.contextPath}/Set`;
|
|
1936
|
+
}
|
|
1937
|
+
if (req.context.parameters.count) {
|
|
1938
|
+
url.contextPath += "/$count";
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function convertRequestBody(body, headers, url, req) {
|
|
1944
|
+
let definition = req.context && req.context.definition;
|
|
1945
|
+
if (definition) {
|
|
1946
|
+
if (definition.kind === "action") {
|
|
1947
|
+
body = Object.assign({}, body, req.context.bodyParameters);
|
|
1948
|
+
definition = {
|
|
1949
|
+
elements: definition.params || {},
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
convertRequestData(body, headers, definition, req);
|
|
1953
|
+
}
|
|
1954
|
+
return JSON.stringify(body);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function convertRequestData(data, headers, definition, req) {
|
|
1958
|
+
if (!Array.isArray(data)) {
|
|
1959
|
+
return convertRequestData([data], headers, definition, req);
|
|
1960
|
+
}
|
|
1961
|
+
const elements = definitionElements(definition);
|
|
1962
|
+
if (structureKeys(elements).length === 0) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
// Modify Payload
|
|
1966
|
+
data.forEach((data) => {
|
|
1967
|
+
delete data.__metadata;
|
|
1968
|
+
delete data.__count;
|
|
1969
|
+
convertDataTypesToV4(data, headers, definition, elements, req);
|
|
1970
|
+
});
|
|
1971
|
+
// Recursion
|
|
1972
|
+
data.forEach((data) => {
|
|
1973
|
+
Object.keys(data).forEach((key) => {
|
|
1974
|
+
const element = elements[key];
|
|
1975
|
+
const type = elementType(element, req);
|
|
1976
|
+
if (element && (type === "cds.Composition" || type === "cds.Association")) {
|
|
1977
|
+
if (data[key] && data[key].__deferred) {
|
|
1978
|
+
delete data[key];
|
|
1979
|
+
} else {
|
|
1980
|
+
if (data[key] !== null) {
|
|
1981
|
+
if (Array.isArray(data[key].results)) {
|
|
1982
|
+
data[key] = data[key].results;
|
|
1983
|
+
}
|
|
1984
|
+
convertRequestData(data[key], headers, element._target, req);
|
|
1985
|
+
} else {
|
|
1986
|
+
delete data[key];
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function convertDataTypesToV4(data, headers, definition, elements, req) {
|
|
1995
|
+
Object.keys(data || {}).forEach((key) => {
|
|
1996
|
+
if (data[key] === null) {
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const element = elements[key];
|
|
2000
|
+
if (element) {
|
|
2001
|
+
data[key] = convertDataTypeToV4(data[key], elementType(element, req), definition, headers);
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
function replaceConvertDataTypeToV4(value, type, definition, headers = {}) {
|
|
2007
|
+
if (value === null || value === undefined) {
|
|
2008
|
+
return value;
|
|
2009
|
+
}
|
|
2010
|
+
if (Array.isArray(value)) {
|
|
2011
|
+
return value.map((entry) => {
|
|
2012
|
+
return replaceConvertDataTypeToV4(entry, type, definition, headers);
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
if (DataTypeMap[type]) {
|
|
2016
|
+
value = value.replace(DataTypeMap[type].v4, "$1");
|
|
2017
|
+
}
|
|
2018
|
+
return convertDataTypeToV4(value, type);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function convertDataTypeToV4(value, type, definition, headers = {}) {
|
|
2022
|
+
if (value === null || value === undefined) {
|
|
2023
|
+
return value;
|
|
2024
|
+
}
|
|
2025
|
+
if (Array.isArray(value)) {
|
|
2026
|
+
return value.map((entry) => {
|
|
2027
|
+
return convertDataTypeToV4(entry, type, definition, headers);
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
const contentType = headers["content-type"];
|
|
2031
|
+
const ieee754Compatible = contentType && contentType.includes(IEEE754Compatible);
|
|
2032
|
+
if (["cds.Boolean"].includes(type)) {
|
|
2033
|
+
if (value === "true") {
|
|
2034
|
+
value = true;
|
|
2035
|
+
} else if (value === "false") {
|
|
2036
|
+
value = false;
|
|
2037
|
+
}
|
|
2038
|
+
} else if (["cds.Integer"].includes(type)) {
|
|
2039
|
+
value = parseInt(value, 10);
|
|
2040
|
+
} else if (["cds.Integer64", "cds.Decimal", "cds.DecimalFloat"].includes(type)) {
|
|
2041
|
+
value = ieee754Compatible ? `${value}` : parseFloat(value);
|
|
2042
|
+
} else if (["cds.Double"].includes(type)) {
|
|
2043
|
+
value = parseFloat(value);
|
|
2044
|
+
} else if (["cds.Time"].includes(type)) {
|
|
2045
|
+
const match = value.match(DurationRegex);
|
|
2046
|
+
if (match) {
|
|
2047
|
+
value = `${match[4] || "00"}:${match[5] || "00"}:${match[6] || "00"}`;
|
|
2048
|
+
}
|
|
2049
|
+
} else if (["cds.Date", "cds.DateTime", "cds.Timestamp"].includes(type)) {
|
|
2050
|
+
const match = value.match(/\/Date\((.*)\)\//is);
|
|
2051
|
+
const ticksAndOffset = match && match.pop();
|
|
2052
|
+
if (ticksAndOffset !== undefined && ticksAndOffset !== null) {
|
|
2053
|
+
value = new Date(calculateTicksOffsetSum(ticksAndOffset)).toISOString(); // always UTC
|
|
2054
|
+
}
|
|
2055
|
+
if (["cds.DateTime"].includes(type)) {
|
|
2056
|
+
value = value.slice(0, 19) + "Z"; // Cut millis
|
|
2057
|
+
} else if (["cds.Date"].includes(type)) {
|
|
2058
|
+
value = value.slice(0, 10); // Cut time
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return value;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function calculateTicksOffsetSum(text) {
|
|
2065
|
+
return (text.replace(/\s/g, "").match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => {
|
|
2066
|
+
return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000); // ticks are milliseconds (0), offset are minutes (1)
|
|
2067
|
+
}, 0);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
function initContext(req, index = 0) {
|
|
2071
|
+
req.context = req.contexts[index] || {};
|
|
2072
|
+
return req.context.definition && req.context.definition.kind === "entity"
|
|
2073
|
+
? req.context.definition
|
|
2074
|
+
: req.context.boundDefinition;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* Convert Proxy Response (v4 -> v2)
|
|
2079
|
+
* @param proxyRes Proxy Request
|
|
2080
|
+
* @param req Request
|
|
2081
|
+
* @param res Response
|
|
2082
|
+
*/
|
|
2083
|
+
async function convertProxyResponse(proxyRes, req, res) {
|
|
2084
|
+
try {
|
|
2085
|
+
req.context = {};
|
|
2086
|
+
let statusCode = proxyRes.statusCode;
|
|
2087
|
+
let headers = proxyRes.headers;
|
|
2088
|
+
if (statusCode < 400 && req.overwriteResponse) {
|
|
2089
|
+
statusCode = req.overwriteResponse.statusCode;
|
|
2090
|
+
headers = {
|
|
2091
|
+
...req.overwriteResponse.headers,
|
|
2092
|
+
...headers,
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
normalizeContentType(headers);
|
|
2097
|
+
|
|
2098
|
+
// Pipe Binary Stream
|
|
2099
|
+
const contentType = headers["content-type"];
|
|
2100
|
+
const transferEncoding = headers["transfer-encoding"] || "";
|
|
2101
|
+
if (transferEncoding.includes("chunked") && !isApplicationJSON(contentType) && !isMultipartMixed(contentType)) {
|
|
2102
|
+
return await processStreamResponse(proxyRes, req, res, headers);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// Body
|
|
2106
|
+
let body = await parseProxyResponseBody(proxyRes, headers, req);
|
|
2107
|
+
if (statusCode < 400 && req.overwriteResponse) {
|
|
2108
|
+
body = {
|
|
2109
|
+
...req.overwriteResponse.body,
|
|
2110
|
+
...body,
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Trace
|
|
2115
|
+
traceResponse(req, "ProxyResponse", proxyRes.statusCode, proxyRes.statusMessage, headers, body);
|
|
2116
|
+
delete headers["content-encoding"];
|
|
2117
|
+
|
|
2118
|
+
convertBasicHeaders(headers);
|
|
2119
|
+
if (body && statusCode < 400) {
|
|
2120
|
+
if (isMultipartMixed(contentType)) {
|
|
2121
|
+
// Multipart
|
|
2122
|
+
const resContentIdOrder = [];
|
|
2123
|
+
body = processMultipartMixed(
|
|
2124
|
+
req,
|
|
2125
|
+
body,
|
|
2126
|
+
contentType,
|
|
2127
|
+
null,
|
|
2128
|
+
({ index, statusCode, contentType, body, headers }) => {
|
|
2129
|
+
const serviceDefinition = initContext(req, index);
|
|
2130
|
+
if (statusCode < 400) {
|
|
2131
|
+
convertHeaders(body, headers, serviceDefinition, req);
|
|
2132
|
+
if (body && isApplicationJSON(contentType)) {
|
|
2133
|
+
body = convertResponseBody(Object.assign({}, body), headers, req);
|
|
2134
|
+
}
|
|
2135
|
+
} else {
|
|
2136
|
+
convertHeaders(body, headers, serviceDefinition, req);
|
|
2137
|
+
body = convertResponseError(body, headers, serviceDefinition, req);
|
|
2138
|
+
}
|
|
2139
|
+
return { body, headers };
|
|
2140
|
+
},
|
|
2141
|
+
resContentIdOrder,
|
|
2142
|
+
ProcessingDirection.Response
|
|
2143
|
+
);
|
|
2144
|
+
if (
|
|
2145
|
+
!(
|
|
2146
|
+
req.contentIdOrder.length === resContentIdOrder.length &&
|
|
2147
|
+
req.contentIdOrder.every((contentId, index) => contentId === resContentIdOrder[index])
|
|
2148
|
+
)
|
|
2149
|
+
) {
|
|
2150
|
+
logWarning(req, "Batch", "Response changeset order does not match request changeset order", {
|
|
2151
|
+
requestContentIds: req.contentIdOrder,
|
|
2152
|
+
responseContentIds: resContentIdOrder,
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
if (statusCode === 200) {
|
|
2156
|
+
// OData V4: 200 => OData V2: 202
|
|
2157
|
+
statusCode = 202;
|
|
2158
|
+
}
|
|
2159
|
+
} else {
|
|
2160
|
+
// Single
|
|
2161
|
+
const serviceDefinition = initContext(req);
|
|
2162
|
+
convertHeaders(body, headers, serviceDefinition, req);
|
|
2163
|
+
if (isApplicationJSON(contentType)) {
|
|
2164
|
+
body = convertResponseBody(Object.assign({}, body), headers, req);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
if (body && !(headers["transfer-encoding"] || "").includes("chunked") && statusCode !== 204) {
|
|
2168
|
+
setContentLength(headers, body);
|
|
2169
|
+
}
|
|
2170
|
+
} else {
|
|
2171
|
+
// Failed
|
|
2172
|
+
const serviceDefinition = initContext(req);
|
|
2173
|
+
convertHeaders(body, headers, serviceDefinition, req);
|
|
2174
|
+
body = convertResponseError(body, headers, serviceDefinition, req);
|
|
2175
|
+
setContentLength(headers, body);
|
|
2176
|
+
}
|
|
2177
|
+
respond(req, res, statusCode, headers, body);
|
|
2178
|
+
} catch (err) {
|
|
2179
|
+
// Error
|
|
2180
|
+
logError(req, "Response", err);
|
|
2181
|
+
if (proxyRes.body && proxyRes.body.error) {
|
|
2182
|
+
respond(
|
|
2183
|
+
req,
|
|
2184
|
+
res,
|
|
2185
|
+
proxyRes.statusCode,
|
|
2186
|
+
proxyRes.headers,
|
|
2187
|
+
convertResponseError(proxyRes.body, proxyRes.headers, undefined, req)
|
|
2188
|
+
);
|
|
2189
|
+
} else {
|
|
2190
|
+
res.status(500).send("Internal Server Error");
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
async function processStreamResponse(proxyRes, req, res, headers) {
|
|
2196
|
+
// Trace
|
|
2197
|
+
traceResponse(req, "ProxyResponse", proxyRes.statusCode, proxyRes.statusMessage, headers, {});
|
|
2198
|
+
|
|
2199
|
+
let streamRes = proxyRes;
|
|
2200
|
+
convertBasicHeaders(headers);
|
|
2201
|
+
const context = req.contexts && req.contexts[0];
|
|
2202
|
+
if (context && context.definition && context.definitionElements) {
|
|
2203
|
+
const mediaDataElementName = findElementByAnnotation(context.definitionElements, "@Core.MediaType");
|
|
2204
|
+
if (mediaDataElementName) {
|
|
2205
|
+
const parts = proxyRes.req.path.split("/");
|
|
2206
|
+
if (parts[parts.length - 1] === "$value" || parts[parts.length - 1].startsWith("$value?")) {
|
|
2207
|
+
parts.pop();
|
|
2208
|
+
}
|
|
2209
|
+
if (parts[parts.length - 1] === mediaDataElementName) {
|
|
2210
|
+
parts.pop();
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// Is Url
|
|
2214
|
+
const urlElement =
|
|
2215
|
+
findElementByAnnotation(context.definitionElements, "@Core.IsURL") ||
|
|
2216
|
+
findElementByAnnotation(context.definitionElements, "@Core.IsUrl");
|
|
2217
|
+
if (urlElement) {
|
|
2218
|
+
const mediaResponse = await fetch(target + parts.join("/"), {
|
|
2219
|
+
method: "GET",
|
|
2220
|
+
headers: propagateHeaders(req, {
|
|
2221
|
+
accept: "application/json",
|
|
2222
|
+
}),
|
|
2223
|
+
});
|
|
2224
|
+
if (!mediaResponse.ok) {
|
|
2225
|
+
throw new Error(await mediaResponse.text());
|
|
2226
|
+
}
|
|
2227
|
+
if (mediaResponse) {
|
|
2228
|
+
const mediaResult = await mediaResponse.json();
|
|
2229
|
+
const mediaReadLink = mediaResult[`${urlElement}@odata.mediaReadLink`];
|
|
2230
|
+
if (mediaReadLink) {
|
|
2231
|
+
try {
|
|
2232
|
+
const mediaResponse = await fetch(mediaReadLink, {
|
|
2233
|
+
method: "GET",
|
|
2234
|
+
headers: propagateHeaders(req),
|
|
2235
|
+
});
|
|
2236
|
+
res.status(mediaResponse.status);
|
|
2237
|
+
headers = convertBasicHeaders(convertToNodeHeaders(mediaResponse.headers));
|
|
2238
|
+
streamRes = mediaResponse.body;
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
logError(req, "MediaStream", err);
|
|
2241
|
+
const errorBody = convertResponseError({ error: err }, {}, context.definition, req);
|
|
2242
|
+
respond(req, res, 500, { "content-type": "application/json" }, errorBody);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
} else {
|
|
2248
|
+
if (!headers["content-disposition"] || calcContentDisposition) {
|
|
2249
|
+
// Is Binary
|
|
2250
|
+
const contentDispositionFilenameElement =
|
|
2251
|
+
findElementValueByAnnotation(context.definitionElements, "@Core.ContentDisposition.Filename") ||
|
|
2252
|
+
findElementValueByAnnotation(context.definitionElements, "@Common.ContentDisposition.Filename");
|
|
2253
|
+
if (contentDispositionFilenameElement) {
|
|
2254
|
+
const contentDispositionTypeValue =
|
|
2255
|
+
findElementValueByAnnotation(context.definitionElements, "@Core.ContentDisposition.Type") ||
|
|
2256
|
+
findElementValueByAnnotation(context.definitionElements, "@Common.ContentDisposition.Type") ||
|
|
2257
|
+
contentDisposition;
|
|
2258
|
+
const response = await fetch(target + [...parts, contentDispositionFilenameElement, "$value"].join("/"), {
|
|
2259
|
+
method: "GET",
|
|
2260
|
+
headers: propagateHeaders(req, {
|
|
2261
|
+
accept: "application/json,*/*",
|
|
2262
|
+
}),
|
|
2263
|
+
});
|
|
2264
|
+
if (response.ok) {
|
|
2265
|
+
const filename = await response.text();
|
|
2266
|
+
if (filename) {
|
|
2267
|
+
headers["content-disposition"] = `${contentDispositionTypeValue}; filename="${encodeURIComponent(
|
|
2268
|
+
filename
|
|
2269
|
+
)}"`;
|
|
2270
|
+
}
|
|
2271
|
+
} else {
|
|
2272
|
+
logWarning(req, "ContentDisposition", await response.text());
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
delete headers["content-encoding"];
|
|
2281
|
+
Object.entries(headers).forEach(([name, value]) => {
|
|
2282
|
+
res.setHeader(name, value);
|
|
2283
|
+
});
|
|
2284
|
+
streamRes.pipe(res);
|
|
2285
|
+
|
|
2286
|
+
// Trace
|
|
2287
|
+
traceResponse(req, "Response", res.statusCode, res.statusMessage, headers, {});
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
async function parseProxyResponseBody(proxyRes, headers, req) {
|
|
2291
|
+
const contentType = headers["content-type"];
|
|
2292
|
+
let bodyParser;
|
|
2293
|
+
if (req.method === "HEAD") {
|
|
2294
|
+
bodyParser = null;
|
|
2295
|
+
} else if (isApplicationJSON(contentType)) {
|
|
2296
|
+
bodyParser = express.json({ limit: bodyParserLimit });
|
|
2297
|
+
} else if (isPlainText(contentType) || isXML(contentType)) {
|
|
2298
|
+
bodyParser = express.text({ type: () => true, limit: bodyParserLimit });
|
|
2299
|
+
} else if (isMultipartMixed(contentType)) {
|
|
2300
|
+
bodyParser = express.text({ type: "multipart/mixed", limit: bodyParserLimit });
|
|
2301
|
+
}
|
|
2302
|
+
if (bodyParser) {
|
|
2303
|
+
await promisify(bodyParser)(proxyRes, null);
|
|
2304
|
+
return proxyRes.body;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function convertBasicHeaders(headers) {
|
|
2309
|
+
delete headers["odata-version"];
|
|
2310
|
+
delete headers["OData-Version"];
|
|
2311
|
+
delete headers["odata-entityid"];
|
|
2312
|
+
delete headers["OData-EntityId"];
|
|
2313
|
+
headers.dataserviceversion = "2.0";
|
|
2314
|
+
return headers;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
function convertHeaders(body, headers, serviceDefinition, req) {
|
|
2318
|
+
convertBasicHeaders(headers);
|
|
2319
|
+
const definition = contextFromBody(body, req);
|
|
2320
|
+
if (definition && definition.kind === "entity") {
|
|
2321
|
+
const elements = definitionElements(definition);
|
|
2322
|
+
convertLocation(body, headers, definition, elements, req);
|
|
2323
|
+
}
|
|
2324
|
+
convertMessages(body, headers, definition && definition.kind === "entity" ? definition : serviceDefinition, req);
|
|
2325
|
+
return headers;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
function convertLocation(body, headers, definition, elements, req) {
|
|
2329
|
+
if (headers.location || headers.Location) {
|
|
2330
|
+
headers.location = entityUri(body, definition, elements, req);
|
|
2331
|
+
}
|
|
2332
|
+
delete headers.Location;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
function convertMessages(body, headers, definition, req) {
|
|
2336
|
+
if (headers["sap-messages"]) {
|
|
2337
|
+
const messages = JSON.parse(headers["sap-messages"]);
|
|
2338
|
+
if (messages && messages.length > 0) {
|
|
2339
|
+
const rootMessage = messages.shift();
|
|
2340
|
+
rootMessage.details = Array.isArray(rootMessage.details) ? rootMessage.details : [];
|
|
2341
|
+
rootMessage.details.push(...messages);
|
|
2342
|
+
const message = convertMessage(rootMessage, definition, req);
|
|
2343
|
+
headers["sap-message"] = JSON.stringify(message);
|
|
2344
|
+
}
|
|
2345
|
+
delete headers["sap-messages"];
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
function convertMessage(message, definition, req, contentID) {
|
|
2350
|
+
if (!message) {
|
|
2351
|
+
return message;
|
|
2352
|
+
}
|
|
2353
|
+
message.severity = SeverityMap[message["@Common.numericSeverity"] || message.numericSeverity] || "error";
|
|
2354
|
+
delete message.numericSeverity;
|
|
2355
|
+
delete message["@Common.numericSeverity"];
|
|
2356
|
+
if (message.target) {
|
|
2357
|
+
message.target = convertMessageTarget(message.target, req, definition);
|
|
2358
|
+
} else if (message.target === undefined && messageTargetDefault) {
|
|
2359
|
+
message.target = messageTargetDefault;
|
|
2360
|
+
}
|
|
2361
|
+
if (Array.isArray(message["@Common.additionalTargets"])) {
|
|
2362
|
+
message.additionalTargets = message["@Common.additionalTargets"].map((messageTarget) => {
|
|
2363
|
+
return convertMessageTarget(messageTarget, req, definition);
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
delete message["@Common.additionalTargets"];
|
|
2367
|
+
contentID = message["@Core.ContentID"] || contentID;
|
|
2368
|
+
message.ContentID = contentID;
|
|
2369
|
+
delete message["@Core.ContentID"];
|
|
2370
|
+
if (Array.isArray(message.details)) {
|
|
2371
|
+
message.details = message.details.map((detail) => {
|
|
2372
|
+
return convertMessage(detail, definition, req, contentID);
|
|
2373
|
+
});
|
|
2374
|
+
if (propagateMessageToDetails) {
|
|
2375
|
+
const propagatedDetailMessage = Object.assign({}, message);
|
|
2376
|
+
delete propagatedDetailMessage.details;
|
|
2377
|
+
message.details.unshift(propagatedDetailMessage);
|
|
2378
|
+
}
|
|
2379
|
+
message.details.forEach((detail) => {
|
|
2380
|
+
if (detail.code && detail.code.toLowerCase().includes("transition")) {
|
|
2381
|
+
detail.transition = true;
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
return message;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function convertMessageTarget(messageTarget, req, definition) {
|
|
2389
|
+
if (!messageTarget) {
|
|
2390
|
+
return messageTarget;
|
|
2391
|
+
}
|
|
2392
|
+
if (req.context.operation && req.context.boundDefinition) {
|
|
2393
|
+
const bindingParamaterName = req.context.operation["@cds.odata.bindingparameter.name"] || "in";
|
|
2394
|
+
if (messageTarget.startsWith(`${bindingParamaterName}/`)) {
|
|
2395
|
+
messageTarget = messageTarget.substr(bindingParamaterName.length + 1);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
let context;
|
|
2399
|
+
definition = definition && definition.kind === "entity" ? definition : undefined;
|
|
2400
|
+
if (
|
|
2401
|
+
contextFromUrl(
|
|
2402
|
+
{
|
|
2403
|
+
contextPath: messageTarget,
|
|
2404
|
+
query: {},
|
|
2405
|
+
},
|
|
2406
|
+
req,
|
|
2407
|
+
undefined,
|
|
2408
|
+
true
|
|
2409
|
+
)
|
|
2410
|
+
) {
|
|
2411
|
+
context = undefined;
|
|
2412
|
+
} else if (
|
|
2413
|
+
contextFromUrl(
|
|
2414
|
+
{
|
|
2415
|
+
contextPath: messageTarget,
|
|
2416
|
+
query: {},
|
|
2417
|
+
},
|
|
2418
|
+
req,
|
|
2419
|
+
definition,
|
|
2420
|
+
true
|
|
2421
|
+
)
|
|
2422
|
+
) {
|
|
2423
|
+
context = definition;
|
|
2424
|
+
}
|
|
2425
|
+
let stop = false;
|
|
2426
|
+
return messageTarget
|
|
2427
|
+
.split("/")
|
|
2428
|
+
.map((part) => {
|
|
2429
|
+
if (stop) {
|
|
2430
|
+
return part;
|
|
2431
|
+
}
|
|
2432
|
+
let keyPart = "";
|
|
2433
|
+
const keyStart = part.indexOf("(");
|
|
2434
|
+
const keyEnd = part.lastIndexOf(")");
|
|
2435
|
+
if (keyStart !== -1 && keyEnd === part.length - 1) {
|
|
2436
|
+
keyPart = part.substring(keyStart + 1, keyEnd);
|
|
2437
|
+
part = part.substr(0, keyStart);
|
|
2438
|
+
}
|
|
2439
|
+
context = lookupContext(part, context, req);
|
|
2440
|
+
if (!context) {
|
|
2441
|
+
stop = true;
|
|
2442
|
+
}
|
|
2443
|
+
const contextElements = definitionElements(context);
|
|
2444
|
+
const contextKeys = definitionKeys(context);
|
|
2445
|
+
if (context && keyPart) {
|
|
2446
|
+
const keys = keyPart.split(",");
|
|
2447
|
+
return `${part}(${keys
|
|
2448
|
+
.map((key) => {
|
|
2449
|
+
const [name, value] = key.split("=");
|
|
2450
|
+
let type;
|
|
2451
|
+
if (name && value) {
|
|
2452
|
+
if (context.params && context.params[name]) {
|
|
2453
|
+
type = context.params[name].type;
|
|
2454
|
+
}
|
|
2455
|
+
if (!type) {
|
|
2456
|
+
type = elementType(contextElements[name], req);
|
|
2457
|
+
}
|
|
2458
|
+
return `${name}=${replaceConvertDataTypeToV2(value, type, context)}`;
|
|
2459
|
+
} else if (name) {
|
|
2460
|
+
const key = structureKeys(contextKeys).find((key) => {
|
|
2461
|
+
return contextKeys[key].type !== "cds.Composition" && contextKeys[key].type !== "cds.Association";
|
|
2462
|
+
});
|
|
2463
|
+
type = key && elementType(contextElements[key], req);
|
|
2464
|
+
return type && `${replaceConvertDataTypeToV2(name, type, context)}`;
|
|
2465
|
+
}
|
|
2466
|
+
return "";
|
|
2467
|
+
})
|
|
2468
|
+
.filter((part) => !!part)
|
|
2469
|
+
.join(",")})`;
|
|
2470
|
+
} else {
|
|
2471
|
+
return part;
|
|
2472
|
+
}
|
|
2473
|
+
})
|
|
2474
|
+
.join("/");
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
function convertResponseError(body, headers, definition, req) {
|
|
2478
|
+
if (!body) {
|
|
2479
|
+
return body;
|
|
2480
|
+
}
|
|
2481
|
+
if (body.error) {
|
|
2482
|
+
if (body.error.message) {
|
|
2483
|
+
body.error.message = {
|
|
2484
|
+
lang: headers["content-language"] || "en",
|
|
2485
|
+
value: body.error.message,
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
body.error = convertMessage(body.error, definition, req);
|
|
2489
|
+
body.error.innererror = body.error.innererror || {};
|
|
2490
|
+
body.error.innererror.errordetails = body.error.innererror.errordetails || [];
|
|
2491
|
+
body.error.innererror.errordetails.push(...(body.error.details || []));
|
|
2492
|
+
delete body.error.details;
|
|
2493
|
+
if (body.error.innererror.errordetails.length === 0) {
|
|
2494
|
+
const singleDetailError = Object.assign({}, body.error);
|
|
2495
|
+
delete singleDetailError.innererror;
|
|
2496
|
+
delete singleDetailError.details;
|
|
2497
|
+
if (body.error.innererror.errordetails.length === 0) {
|
|
2498
|
+
body.error.innererror.errordetails.push(singleDetailError);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
if (typeof body === "object") {
|
|
2503
|
+
body = JSON.stringify(body);
|
|
2504
|
+
}
|
|
2505
|
+
body = `${body}`;
|
|
2506
|
+
setContentLength(headers, body);
|
|
2507
|
+
return body;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function convertResponseBody(proxyBody, headers, req) {
|
|
2511
|
+
const body = {
|
|
2512
|
+
d: {},
|
|
2513
|
+
};
|
|
2514
|
+
if (req.context.serviceRoot && proxyBody.value) {
|
|
2515
|
+
if (req.context.serviceRootAsXML) {
|
|
2516
|
+
// Service Root XML
|
|
2517
|
+
let xmlBody = `<?xml version="1.0" encoding="utf-8" standalone="yes" ?>`;
|
|
2518
|
+
xmlBody += `<service xml:base="${serviceUri(
|
|
2519
|
+
req
|
|
2520
|
+
)}" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">`;
|
|
2521
|
+
xmlBody += `<workspace><atom:title>Default</atom:title>`;
|
|
2522
|
+
xmlBody += proxyBody.value
|
|
2523
|
+
.map((entry) => {
|
|
2524
|
+
return `<collection href="${entry.name}"><atom:title>${entry.name}</atom:title></collection>`;
|
|
2525
|
+
})
|
|
2526
|
+
.join("");
|
|
2527
|
+
xmlBody += `</workspace></service>`;
|
|
2528
|
+
headers["content-type"] = "application/xml";
|
|
2529
|
+
return xmlBody;
|
|
2530
|
+
} else {
|
|
2531
|
+
// Service Root JSON
|
|
2532
|
+
body.d.EntitySets = proxyBody.value.map((entry) => {
|
|
2533
|
+
return entry.name;
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
} else {
|
|
2537
|
+
// Context from Body
|
|
2538
|
+
const definition = contextFromBody(proxyBody, req);
|
|
2539
|
+
if (definition) {
|
|
2540
|
+
req.context.requestDefinition = req.context.definition;
|
|
2541
|
+
req.context.definition = definition;
|
|
2542
|
+
req.context.definitionElements = definitionElements(definition);
|
|
2543
|
+
const elements = req.context.definitionElements;
|
|
2544
|
+
const definitionElement = contextElementFromBody(proxyBody, req);
|
|
2545
|
+
if (definitionElement) {
|
|
2546
|
+
body.d[definitionElement.name] = proxyBody.value;
|
|
2547
|
+
convertResponseElementData(body, headers, definition, elements, proxyBody, req);
|
|
2548
|
+
if (req.context.$value) {
|
|
2549
|
+
headers["content-type"] = "text/plain";
|
|
2550
|
+
return `${body.d[definitionElement.name]}`;
|
|
2551
|
+
}
|
|
2552
|
+
} else {
|
|
2553
|
+
const data = convertResponseList(body, headers, definition, proxyBody, req);
|
|
2554
|
+
convertResponseData(data, headers, definition, proxyBody, req);
|
|
2555
|
+
}
|
|
2556
|
+
} else {
|
|
2557
|
+
// Context from Request
|
|
2558
|
+
let definition = req.context.definition;
|
|
2559
|
+
if (definition && (definition.kind === "function" || definition.kind === "action")) {
|
|
2560
|
+
const returnDefinition = req.context.returnDefinition;
|
|
2561
|
+
if (!returnDefinition || returnDefinition.name) {
|
|
2562
|
+
definition = returnDefinition;
|
|
2563
|
+
} else {
|
|
2564
|
+
definition = {
|
|
2565
|
+
kind: "type",
|
|
2566
|
+
name: contextNameFromBody(proxyBody),
|
|
2567
|
+
...returnDefinition,
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
req.context.requestDefinition = req.context.definition;
|
|
2571
|
+
req.context.definition = definition;
|
|
2572
|
+
req.context.definitionElements = definitionElements(definition);
|
|
2573
|
+
}
|
|
2574
|
+
const data = convertResponseList(body, headers, definition, proxyBody, req);
|
|
2575
|
+
convertResponseData(data, headers, definition, proxyBody, req);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
if (req.context.operation) {
|
|
2580
|
+
const localOperationName = localName(req.context.operation.name);
|
|
2581
|
+
const isArrayResult = Array.isArray(body.d.results) || Array.isArray(body.d);
|
|
2582
|
+
if (req.context.definition.kind === "type") {
|
|
2583
|
+
if (returnComplexNested && !isArrayResult) {
|
|
2584
|
+
body.d = {
|
|
2585
|
+
[localOperationName]: body.d,
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
} else if (!req.context.definition.kind && req.context.definition.name && req.context.definitionElements.value) {
|
|
2589
|
+
if (returnPrimitivePlain) {
|
|
2590
|
+
body.d = isArrayResult ? (body.d.results || body.d).map((entry) => entry.value) : body.d.value;
|
|
2591
|
+
}
|
|
2592
|
+
if (returnPrimitiveNested) {
|
|
2593
|
+
body.d = isArrayResult
|
|
2594
|
+
? {
|
|
2595
|
+
results: body.d,
|
|
2596
|
+
}
|
|
2597
|
+
: {
|
|
2598
|
+
[localOperationName]: body.d,
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
return JSON.stringify(body);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
function convertResponseList(body, headers, definition, proxyBody, req) {
|
|
2608
|
+
if (Array.isArray(proxyBody.value)) {
|
|
2609
|
+
if (req.context.aggregationKey) {
|
|
2610
|
+
proxyBody = proxyBody.value[0] || {};
|
|
2611
|
+
} else {
|
|
2612
|
+
body.d.results = proxyBody.value || [];
|
|
2613
|
+
if (req.context.$count) {
|
|
2614
|
+
body.d.__count = String(proxyBody["@odata.count"] || proxyBody["@count"] || 0);
|
|
2615
|
+
}
|
|
2616
|
+
if (proxyBody["@odata.nextLink"] !== undefined || proxyBody["@nextLink"] !== undefined) {
|
|
2617
|
+
const skipToken = URL.parse(proxyBody["@odata.nextLink"] || proxyBody["@nextLink"], true).query["$skiptoken"];
|
|
2618
|
+
if (skipToken) {
|
|
2619
|
+
body.d.__next = linkUri(req, {
|
|
2620
|
+
$skiptoken: skipToken,
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
let deltaToken;
|
|
2625
|
+
if (proxyBody["@odata.deltaLink"] !== undefined || proxyBody["@deltaLink"] !== undefined) {
|
|
2626
|
+
deltaToken = URL.parse(proxyBody["@odata.deltaLink"] || proxyBody["@deltaLink"], true).query["!deltatoken"];
|
|
2627
|
+
}
|
|
2628
|
+
if (!deltaToken && definition && definition["@cov2ap.deltaResponse"] === "timestamp") {
|
|
2629
|
+
deltaToken = `'${new Date().getTime()}'`;
|
|
2630
|
+
}
|
|
2631
|
+
if (deltaToken) {
|
|
2632
|
+
body.d.__delta = linkUri(req, {
|
|
2633
|
+
"!deltatoken": deltaToken,
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
body.d.results = body.d.results.map((entry) => {
|
|
2637
|
+
return typeof entry == "object" ? entry : { value: entry };
|
|
2638
|
+
});
|
|
2639
|
+
if (req.context.parameters) {
|
|
2640
|
+
if (req.context.parameters.kind === "Parameters") {
|
|
2641
|
+
body.d.results = body.d.results.slice(0, 1);
|
|
2642
|
+
} else if (req.context.parameters.kind === "Set") {
|
|
2643
|
+
if (Object.keys(req.context.parameters.keys).length > 0) {
|
|
2644
|
+
body.d.results = body.d.results.filter((entry) => {
|
|
2645
|
+
return Object.keys(req.context.parameters.keys).every((key) => {
|
|
2646
|
+
return entry[key] === req.context.parameters.keys[key];
|
|
2647
|
+
});
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
if (!returnCollectionNested) {
|
|
2653
|
+
body.d = body.d.results;
|
|
2654
|
+
return body.d;
|
|
2655
|
+
}
|
|
2656
|
+
return body.d.results;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
body.d = proxyBody;
|
|
2660
|
+
return [body.d];
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function convertResponseData(data, headers, definition, proxyBody, req) {
|
|
2664
|
+
if (data === null) {
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
if (!Array.isArray(data)) {
|
|
2668
|
+
return convertResponseData([data], headers, definition, proxyBody, req);
|
|
2669
|
+
}
|
|
2670
|
+
if (!definition) {
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
const elements = definitionElements(definition);
|
|
2674
|
+
// Recursion
|
|
2675
|
+
data.forEach((data) => {
|
|
2676
|
+
Object.keys(data).forEach((key) => {
|
|
2677
|
+
let element = elements[key];
|
|
2678
|
+
if (!element) {
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
const type = elementType(element, req);
|
|
2682
|
+
if (type === "cds.Composition" || type === "cds.Association") {
|
|
2683
|
+
convertResponseData(data[key], headers, element._target, proxyBody, req);
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2686
|
+
});
|
|
2687
|
+
// Structural Changes
|
|
2688
|
+
data.forEach((data) => {
|
|
2689
|
+
addResultsNesting(data, headers, definition, elements, proxyBody, req);
|
|
2690
|
+
});
|
|
2691
|
+
// Deferreds
|
|
2692
|
+
data.forEach((data) => {
|
|
2693
|
+
addDeferreds(data, headers, definition, elements, proxyBody, req);
|
|
2694
|
+
});
|
|
2695
|
+
// Modify Payload
|
|
2696
|
+
data.forEach((data) => {
|
|
2697
|
+
addMetadata(data, headers, definition, elements, proxyBody, req);
|
|
2698
|
+
removeMetadata(data, headers, definition, elements, proxyBody, req);
|
|
2699
|
+
convertMedia(data, headers, definition, elements, proxyBody, req);
|
|
2700
|
+
removeAnnotations(data, headers, definition, elements, proxyBody, req);
|
|
2701
|
+
convertDataTypesToV2(data, headers, definition, elements, proxyBody, req);
|
|
2702
|
+
convertAggregation(data, headers, definition, elements, proxyBody, req);
|
|
2703
|
+
filterParameters(data, headers, definition, elements, proxyBody, req);
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function convertResponseElementData(data, headers, definition, elements, proxyBody, req) {
|
|
2708
|
+
if (!definition) {
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
// Modify Payload
|
|
2712
|
+
convertDataTypesToV2(data, headers, definition, elements, proxyBody, req);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function contextNameFromBody(body) {
|
|
2716
|
+
let context = body && (body["@odata.context"] || body["@context"]);
|
|
2717
|
+
if (!context) {
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
context = context.substr(context.indexOf("#") + 1);
|
|
2721
|
+
if (context.startsWith("Collection(")) {
|
|
2722
|
+
context = context.substring("Collection(".length, context.indexOf(")"));
|
|
2723
|
+
} else {
|
|
2724
|
+
if (context.indexOf("(") !== -1) {
|
|
2725
|
+
context = context.substr(0, context.indexOf("("));
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
if (context.indexOf("/") !== -1) {
|
|
2729
|
+
context = context.substr(0, context.indexOf("/"));
|
|
2730
|
+
}
|
|
2731
|
+
return context;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
function contextFromBody(body, req) {
|
|
2735
|
+
const context = contextNameFromBody(body);
|
|
2736
|
+
if (context) {
|
|
2737
|
+
return lookupDefinition(context, req);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function contextElementFromBody(body, req) {
|
|
2742
|
+
let context = body["@odata.context"] || body["@context"];
|
|
2743
|
+
const definition = contextFromBody(body, req);
|
|
2744
|
+
if (!(context && definition)) {
|
|
2745
|
+
return null;
|
|
2746
|
+
}
|
|
2747
|
+
if (context.lastIndexOf("/") !== -1) {
|
|
2748
|
+
const name = context.substr(context.lastIndexOf("/") + 1);
|
|
2749
|
+
if (name && !name.startsWith("$")) {
|
|
2750
|
+
const element = definitionElements(definition)[name];
|
|
2751
|
+
if (element) {
|
|
2752
|
+
return element;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
function addMetadata(data, headers, definition, elements, body, req) {
|
|
2759
|
+
const typeSuffix =
|
|
2760
|
+
definition.kind === "entity" && definition.params && req.context.parameters
|
|
2761
|
+
? req.context.parameters.kind === "Set"
|
|
2762
|
+
? "Type"
|
|
2763
|
+
: "Parameters"
|
|
2764
|
+
: "";
|
|
2765
|
+
data.__metadata = {
|
|
2766
|
+
type: definition.name + typeSuffix,
|
|
2767
|
+
};
|
|
2768
|
+
if (definition.kind === "entity") {
|
|
2769
|
+
data.__metadata.uri = entityUri(data, definition, elements, req);
|
|
2770
|
+
if (data["@odata.etag"] || data["@etag"]) {
|
|
2771
|
+
data.__metadata.etag = data["@odata.etag"] || data["@etag"];
|
|
2772
|
+
}
|
|
2773
|
+
const mediaDataElementName = findElementByAnnotation(elements, "@Core.MediaType");
|
|
2774
|
+
if (mediaDataElementName) {
|
|
2775
|
+
data.__metadata.media_src = `${data.__metadata.uri}/$value`;
|
|
2776
|
+
const mediaDataElement = elements[mediaDataElementName];
|
|
2777
|
+
const mediaTypeElementName =
|
|
2778
|
+
(mediaDataElement["@Core.MediaType"] && mediaDataElement["@Core.MediaType"]["="]) ||
|
|
2779
|
+
findElementByAnnotation(elements, "@Core.IsMediaType");
|
|
2780
|
+
if (mediaTypeElementName) {
|
|
2781
|
+
data.__metadata.content_type = data[mediaTypeElementName];
|
|
2782
|
+
} else if (mediaDataElement["@Core.MediaType"]) {
|
|
2783
|
+
data.__metadata.content_type = mediaDataElement["@Core.MediaType"];
|
|
2784
|
+
}
|
|
2785
|
+
if (!data.__metadata.content_type) {
|
|
2786
|
+
data.__metadata.content_type = "application/octet-stream";
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function removeMetadata(data, headers, definition, elements, body, req) {
|
|
2793
|
+
Object.keys(data).forEach((key) => {
|
|
2794
|
+
if (key.startsWith("@") || key.startsWith("odata.") || key.includes("@odata.")) {
|
|
2795
|
+
delete data[key];
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
function convertMedia(data, headers, definition, elements, proxyBody, req) {
|
|
2801
|
+
Object.keys(data).forEach((key) => {
|
|
2802
|
+
if (key.endsWith("@odata.mediaReadLink")) {
|
|
2803
|
+
data[key.split("@odata.mediaReadLink")[0]] = data[key];
|
|
2804
|
+
} else if (key.endsWith("@mediaReadLink")) {
|
|
2805
|
+
data[key.split("@mediaReadLink")[0]] = data[key];
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
function removeAnnotations(data, headers, definition, elements, proxyBody, req) {
|
|
2811
|
+
Object.keys(data).forEach((key) => {
|
|
2812
|
+
if (key.startsWith("@")) {
|
|
2813
|
+
delete data[key];
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
function convertAggregation(data, headers, definition, elements, body, req) {
|
|
2819
|
+
if (!req.context.$apply) {
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
Object.keys(data).forEach((key) => {
|
|
2823
|
+
let value = data[key];
|
|
2824
|
+
if (key.startsWith(AggregationPrefix)) {
|
|
2825
|
+
if (!(key.endsWith("@odata.type") || key.endsWith("@type"))) {
|
|
2826
|
+
const name = key.substr(AggregationPrefix.length);
|
|
2827
|
+
let aggregationType = (data[`${key}@odata.type`] || data[`${key}@type`] || "#Decimal").replace("#", "");
|
|
2828
|
+
if (DataTypeOData[aggregationType]) {
|
|
2829
|
+
aggregationType = DataTypeOData[aggregationType];
|
|
2830
|
+
} else {
|
|
2831
|
+
aggregationType = `cds.${aggregationType}`;
|
|
2832
|
+
}
|
|
2833
|
+
if (
|
|
2834
|
+
["cds.Integer", "cds.Integer64", "cds.Double", "cds.Decimal", "cds.DecimalFloat"].includes(aggregationType)
|
|
2835
|
+
) {
|
|
2836
|
+
if (value === null || value === "null") {
|
|
2837
|
+
value = 0;
|
|
2838
|
+
}
|
|
2839
|
+
} else if (["cds.String", "cds.LargeString"].includes(aggregationType)) {
|
|
2840
|
+
if (value === null) {
|
|
2841
|
+
value = "";
|
|
2842
|
+
}
|
|
2843
|
+
} else if (["cds.Boolean"].includes(aggregationType)) {
|
|
2844
|
+
if (value === null) {
|
|
2845
|
+
value = false;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
let aggregationValue = convertDataTypeToV2(value, aggregationType, definition);
|
|
2849
|
+
// Convert to JSON number
|
|
2850
|
+
const element = req.context.$apply.value.find((entry) => {
|
|
2851
|
+
return entry.name === name;
|
|
2852
|
+
});
|
|
2853
|
+
if (element && elementType(element, req) === "cds.Integer") {
|
|
2854
|
+
const aggregation = element["@Aggregation.default"] || element["@DefaultAggregation"];
|
|
2855
|
+
const aggregationName = aggregation ? aggregation["#"] || aggregation : DefaultAggregation;
|
|
2856
|
+
const aggregationFunction = aggregationName ? AggregationMap[aggregationName.toUpperCase()] : undefined;
|
|
2857
|
+
if (
|
|
2858
|
+
aggregationType === "cds.Decimal" &&
|
|
2859
|
+
aggregationFunction &&
|
|
2860
|
+
![AggregationMap.AVG, AggregationMap.COUNT_DISTINCT].includes(aggregationFunction)
|
|
2861
|
+
) {
|
|
2862
|
+
const floatValue = parseFloat(aggregationValue);
|
|
2863
|
+
if (aggregationValue === `${floatValue}`) {
|
|
2864
|
+
aggregationValue = floatValue;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
data[name] = aggregationValue;
|
|
2869
|
+
delete data[key];
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
Object.keys(data).forEach((key) => {
|
|
2874
|
+
if (key.startsWith(AggregationPrefix) && (key.endsWith("@odata.type") || key.endsWith("@type"))) {
|
|
2875
|
+
delete data[key];
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
const aggregationKey = {
|
|
2879
|
+
key: req.context.$apply.key.reduce((result, keyElement) => {
|
|
2880
|
+
const type = elementType(keyElement, req);
|
|
2881
|
+
let value = data[keyElement.name];
|
|
2882
|
+
if (value !== undefined && value !== null) {
|
|
2883
|
+
value = encodeURIKey(value);
|
|
2884
|
+
if (DataTypeMap[type]) {
|
|
2885
|
+
value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
result[keyElement.name] = value;
|
|
2889
|
+
return result;
|
|
2890
|
+
}, {}),
|
|
2891
|
+
value: req.context.$apply.value.map((valueElement) => {
|
|
2892
|
+
return valueElement.name;
|
|
2893
|
+
}),
|
|
2894
|
+
};
|
|
2895
|
+
if (req.context.$apply.filter) {
|
|
2896
|
+
aggregationKey.filter = encodeURIComponent(req.context.$apply.filter);
|
|
2897
|
+
}
|
|
2898
|
+
if (req.context.$apply.search) {
|
|
2899
|
+
aggregationKey.search = encodeURIComponent(req.context.$apply.search);
|
|
2900
|
+
}
|
|
2901
|
+
data.ID__ = `aggregation'${JSON.stringify(aggregationKey)}'`;
|
|
2902
|
+
data.__metadata.uri = entityUriKey(data.ID__, definition, req);
|
|
2903
|
+
delete data.__metadata.etag;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
function filterParameters(data, headers, definition, elements, body, req) {
|
|
2907
|
+
if (
|
|
2908
|
+
definition &&
|
|
2909
|
+
definition.kind === "entity" &&
|
|
2910
|
+
definition.params &&
|
|
2911
|
+
req.context.parameters &&
|
|
2912
|
+
req.context.parameters.kind === "Parameters"
|
|
2913
|
+
) {
|
|
2914
|
+
Object.keys(elements).forEach((name) => {
|
|
2915
|
+
if (!definition.params[name]) {
|
|
2916
|
+
delete data[name];
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
function replaceConvertDataTypeToV2(value, type, definition) {
|
|
2923
|
+
if (value === null || value === undefined) {
|
|
2924
|
+
return value;
|
|
2925
|
+
}
|
|
2926
|
+
if (Array.isArray(value)) {
|
|
2927
|
+
return value.map((entry) => {
|
|
2928
|
+
return replaceConvertDataTypeToV2(entry, type, definition);
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
value = convertDataTypeToV2(value, type, definition);
|
|
2932
|
+
if (DataTypeMap[type]) {
|
|
2933
|
+
if (!value.match(DataTypeMap[type].v2.replace("$1", ".*"))) {
|
|
2934
|
+
value = DataTypeMap[type].v2.replace("$1", value);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
return value;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
function convertDataTypesToV2(data, headers, definition, elements, body, req) {
|
|
2941
|
+
Object.keys(data).forEach((key) => {
|
|
2942
|
+
if (data[key] === null) {
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
const element = elements[key];
|
|
2946
|
+
if (!element) {
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
data[key] = convertDataTypeToV2(data[key], elementType(element, req), definition);
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
function convertDataTypeToV2(value, type, definition) {
|
|
2954
|
+
if (value === null || value === undefined) {
|
|
2955
|
+
return value;
|
|
2956
|
+
}
|
|
2957
|
+
if (Array.isArray(value)) {
|
|
2958
|
+
return value.map((entry) => {
|
|
2959
|
+
return convertDataTypeToV2(entry, type, definition);
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
if (["cds.Decimal", "cds.DecimalFloat", "cds.Double", "cds.Integer64"].includes(type)) {
|
|
2963
|
+
value = `${value}`;
|
|
2964
|
+
} else if (!isoDate && !definition["@cov2ap.isoDate"] && ["cds.Date"].includes(type)) {
|
|
2965
|
+
value = `/Date(${new Date(value).getTime()})/`;
|
|
2966
|
+
} else if (!isoTime && !definition["@cov2ap.isoTime"] && ["cds.Time"].includes(type)) {
|
|
2967
|
+
value = convertToDayTimeDuration(value);
|
|
2968
|
+
} else if (
|
|
2969
|
+
!isoDateTime &&
|
|
2970
|
+
!definition["@cov2ap.isoDateTime"] &&
|
|
2971
|
+
!isoDateTimeOffset &&
|
|
2972
|
+
!definition["@cov2ap.isoDateTimeOffset"] &&
|
|
2973
|
+
["cds.DateTime"].includes(type)
|
|
2974
|
+
) {
|
|
2975
|
+
value = `/Date(${new Date(value).getTime()}+0000)/`; // always UTC
|
|
2976
|
+
} else if (
|
|
2977
|
+
!isoTimestamp &&
|
|
2978
|
+
!definition["@cov2ap.isoTimestamp"] &&
|
|
2979
|
+
!isoDateTimeOffset &&
|
|
2980
|
+
!definition["@cov2ap.isoDateTimeOffset"] &&
|
|
2981
|
+
["cds.Timestamp"].includes(type)
|
|
2982
|
+
) {
|
|
2983
|
+
value = `/Date(${new Date(value).getTime()}+0000)/`; // always UTC
|
|
2984
|
+
}
|
|
2985
|
+
return value;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
function convertDataTypeToV2Uri(value, type) {
|
|
2989
|
+
if (["cds.Date"].includes(type)) {
|
|
2990
|
+
value = `${value}T00:00:00`;
|
|
2991
|
+
} else if (["cds.Time"].includes(type)) {
|
|
2992
|
+
value = convertToDayTimeDuration(value);
|
|
2993
|
+
}
|
|
2994
|
+
return value;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
function convertToDayTimeDuration(value) {
|
|
2998
|
+
const timeParts = value.split(":");
|
|
2999
|
+
return `PT${timeParts[0] || "00"}H${timeParts[1] || "00"}M${timeParts[2] || "00"}S`;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
function addResultsNesting(data, headers, definition, elements, root, req) {
|
|
3003
|
+
if (!returnCollectionNested) {
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
Object.keys(data).forEach((key) => {
|
|
3007
|
+
const element = elements[key];
|
|
3008
|
+
if (!element) {
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
if (element.cardinality && element.cardinality.max === "*") {
|
|
3012
|
+
data[key] = {
|
|
3013
|
+
results: data[key],
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function addDeferreds(data, headers, definition, elements, root, req) {
|
|
3020
|
+
if (definition.kind !== "entity" || req.context.$apply) {
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
const _entityUri = entityUri(data, definition, elements, req);
|
|
3024
|
+
for (let key of structureKeys(elements)) {
|
|
3025
|
+
const element = elements[key];
|
|
3026
|
+
const type = elementType(element, req);
|
|
3027
|
+
if (element && (type === "cds.Composition" || type === "cds.Association")) {
|
|
3028
|
+
if (data[key] === undefined) {
|
|
3029
|
+
if (fixDraftRequests && req.context.expandSiblingEntity && key === "SiblingEntity") {
|
|
3030
|
+
data[key] = null;
|
|
3031
|
+
} else {
|
|
3032
|
+
data[key] = {
|
|
3033
|
+
__deferred: {
|
|
3034
|
+
uri: `${_entityUri}/${key}`,
|
|
3035
|
+
},
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
if (definition.kind === "entity" && definition.params && req.context.parameters) {
|
|
3042
|
+
if (req.context.parameters.kind === "Parameters") {
|
|
3043
|
+
data.Set = {
|
|
3044
|
+
__deferred: {
|
|
3045
|
+
uri: `${_entityUri}/Set`,
|
|
3046
|
+
},
|
|
3047
|
+
};
|
|
3048
|
+
} else if (req.context.parameters.kind === "Set") {
|
|
3049
|
+
data.Parameters = {
|
|
3050
|
+
__deferred: {
|
|
3051
|
+
uri: `${_entityUri}/Parameters`,
|
|
3052
|
+
},
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
function rootUri(req) {
|
|
3059
|
+
const protocol = req.header("x-forwarded-proto") || req.protocol || "http";
|
|
3060
|
+
const host =
|
|
3061
|
+
req.header("x-forwarded-host") ||
|
|
3062
|
+
req.headers.host ||
|
|
3063
|
+
`${req.hostname || DefaultHost}:${req.socket.address().port || DefaultPort}`;
|
|
3064
|
+
return `${protocol}://${host}`.replace(/^http:\/\/127.0.0.1/, `http://${DefaultHost}`);
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
function serviceUri(req) {
|
|
3068
|
+
if (req.context.serviceUri) {
|
|
3069
|
+
return req.context.serviceUri;
|
|
3070
|
+
}
|
|
3071
|
+
let serviceUri = rootUri(req);
|
|
3072
|
+
if (req.header("x-forwarded-path") === undefined) {
|
|
3073
|
+
serviceUri += `${sourcePath}/${req.servicePath}`;
|
|
3074
|
+
} else {
|
|
3075
|
+
const path = stripSlashes(URL.parse(req.header("x-forwarded-path") || "").pathname || "");
|
|
3076
|
+
let resourceStartPath = "";
|
|
3077
|
+
const definition = req.context.requestDefinition;
|
|
3078
|
+
if (definition) {
|
|
3079
|
+
if (definition.kind === "entity") {
|
|
3080
|
+
resourceStartPath = localEntityName(definition, req);
|
|
3081
|
+
} else if (definition.kind === "function" || definition.kind === "action") {
|
|
3082
|
+
resourceStartPath = localEntityName(definition.parent || definition, req);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
const parts = [];
|
|
3086
|
+
path.split("/").some((part) => {
|
|
3087
|
+
if (
|
|
3088
|
+
part === resourceStartPath ||
|
|
3089
|
+
part.startsWith(`${resourceStartPath}(`) ||
|
|
3090
|
+
part.startsWith(`${resourceStartPath}?`) ||
|
|
3091
|
+
part === "$batch" ||
|
|
3092
|
+
part.startsWith("$batch?")
|
|
3093
|
+
) {
|
|
3094
|
+
return true;
|
|
3095
|
+
}
|
|
3096
|
+
parts.push(part);
|
|
3097
|
+
return false;
|
|
3098
|
+
});
|
|
3099
|
+
if (parts.length > 0) {
|
|
3100
|
+
serviceUri += `/${parts.join("/")}`;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
req.context.serviceUri = serviceUri.endsWith("/") ? serviceUri : `${serviceUri}/`;
|
|
3104
|
+
return req.context.serviceUri;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
function entityUriCollection(entity, req) {
|
|
3108
|
+
return `${serviceUri(req)}${localEntityName(entity, req)}`;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
function entityUriKey(key, entity, req) {
|
|
3112
|
+
return `${entityUriCollection(entity, req)}(${key})`;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
function entityUri(data, entity, elements, req) {
|
|
3116
|
+
return entityUriKey(entityKey(data, entity, elements, req), entity, req);
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function entityKey(data, entity, elements, req) {
|
|
3120
|
+
if (entity.kind === "entity" && entity.params && req.context.parameters) {
|
|
3121
|
+
return entityKeyParameters(data, entity, elements, req);
|
|
3122
|
+
}
|
|
3123
|
+
const keyElements = structureKeys(definitionKeys(entity)).reduce((keys, key) => {
|
|
3124
|
+
const element = elements[key];
|
|
3125
|
+
const type = elementType(element, req);
|
|
3126
|
+
if (!(type === "cds.Composition" || type === "cds.Association")) {
|
|
3127
|
+
keys.push(element);
|
|
3128
|
+
}
|
|
3129
|
+
return keys;
|
|
3130
|
+
}, []);
|
|
3131
|
+
return keyElements
|
|
3132
|
+
.map((keyElement) => {
|
|
3133
|
+
const type = elementType(keyElement, req);
|
|
3134
|
+
let value = data[keyElement.name];
|
|
3135
|
+
if (value !== undefined && value !== null) {
|
|
3136
|
+
value = encodeURIKey(value);
|
|
3137
|
+
if (DataTypeMap[type]) {
|
|
3138
|
+
value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (keyElements.length === 1) {
|
|
3142
|
+
return `${value}`;
|
|
3143
|
+
} else {
|
|
3144
|
+
return `${keyElement.name}=${value}`;
|
|
3145
|
+
}
|
|
3146
|
+
})
|
|
3147
|
+
.join(",");
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function entityKeyParameters(data, entity, elements, req) {
|
|
3151
|
+
const keys = definitionKeys(entity);
|
|
3152
|
+
const keyElements = [];
|
|
3153
|
+
Object.keys(req.context.parameters.values).forEach((param) => {
|
|
3154
|
+
keyElements.push(entity.params[param]);
|
|
3155
|
+
});
|
|
3156
|
+
if (req.context.parameters.kind === "Set") {
|
|
3157
|
+
Object.keys(req.context.parameters.keys).forEach((key) => {
|
|
3158
|
+
keyElements.push(keys[key]);
|
|
3159
|
+
});
|
|
3160
|
+
const columns = entity.query.SELECT.columns || [];
|
|
3161
|
+
Object.keys(keys).forEach((key) => {
|
|
3162
|
+
const param = columns.find((column) => column.as === key);
|
|
3163
|
+
const paramName = (param ? param.ref.join("_") : "") || key;
|
|
3164
|
+
if (!keyElements.find((keyElement) => keyElement.name === paramName)) {
|
|
3165
|
+
keyElements.push(keys[key]);
|
|
3166
|
+
}
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
data = { ...data, ...req.context.parameters.values, ...req.context.parameters.keys };
|
|
3170
|
+
return keyElements
|
|
3171
|
+
.map((keyElement) => {
|
|
3172
|
+
const type = elementType(keyElement, req);
|
|
3173
|
+
let value = data[keyElement.name];
|
|
3174
|
+
if (value !== undefined && value !== null) {
|
|
3175
|
+
value = encodeURIKey(value);
|
|
3176
|
+
if (DataTypeMap[type]) {
|
|
3177
|
+
value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
if (keyElements.length === 1) {
|
|
3181
|
+
return `${value}`;
|
|
3182
|
+
} else {
|
|
3183
|
+
return `${keyElement.name}=${value}`;
|
|
3184
|
+
}
|
|
3185
|
+
})
|
|
3186
|
+
.join(",");
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
function linkUri(req, params) {
|
|
3190
|
+
const originalUrl = req.context.url.originalUrl;
|
|
3191
|
+
Object.keys(params || {}).forEach((key) => {
|
|
3192
|
+
const value = params[key];
|
|
3193
|
+
if (Array.isArray(value)) {
|
|
3194
|
+
params[key] = value.pop();
|
|
3195
|
+
}
|
|
3196
|
+
if (params[key] === undefined) {
|
|
3197
|
+
delete params[key];
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
return (
|
|
3201
|
+
serviceUri(req) +
|
|
3202
|
+
decodeURIComponent(
|
|
3203
|
+
URL.format({
|
|
3204
|
+
...originalUrl,
|
|
3205
|
+
search: null,
|
|
3206
|
+
pathname: originalUrl.contextPath,
|
|
3207
|
+
query: {
|
|
3208
|
+
...originalUrl.query,
|
|
3209
|
+
...params,
|
|
3210
|
+
},
|
|
3211
|
+
})
|
|
3212
|
+
)
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
function respond(req, res, statusCode, headers, body) {
|
|
3217
|
+
if (!res.headersSent) {
|
|
3218
|
+
if (!body || statusCode === 204) {
|
|
3219
|
+
delete headers["content-length"];
|
|
3220
|
+
}
|
|
3221
|
+
Object.entries(headers).forEach(([name, value]) => {
|
|
3222
|
+
res.setHeader(name, value);
|
|
3223
|
+
});
|
|
3224
|
+
res.status(statusCode);
|
|
3225
|
+
if (body && statusCode !== 204) {
|
|
3226
|
+
res.write(body);
|
|
3227
|
+
}
|
|
3228
|
+
res.end();
|
|
3229
|
+
|
|
3230
|
+
// Trace
|
|
3231
|
+
traceResponse(req, "Response", res.statusCode, res.statusMessage, headers, body);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
function normalizeContentType(headers) {
|
|
3236
|
+
let contentType = headers["content-type"];
|
|
3237
|
+
if (contentType) {
|
|
3238
|
+
contentType = contentType.trim();
|
|
3239
|
+
if (isApplicationJSON(contentType)) {
|
|
3240
|
+
contentType = contentType
|
|
3241
|
+
.split(";")
|
|
3242
|
+
.filter((part) => {
|
|
3243
|
+
return !part.startsWith("odata.");
|
|
3244
|
+
})
|
|
3245
|
+
.join(";");
|
|
3246
|
+
}
|
|
3247
|
+
headers["content-type"] = contentType;
|
|
3248
|
+
}
|
|
3249
|
+
return contentType;
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
function normalizeBody(body) {
|
|
3253
|
+
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
|
3254
|
+
return body;
|
|
3255
|
+
}
|
|
3256
|
+
if (typeof body === "object") {
|
|
3257
|
+
return JSON.stringify(body);
|
|
3258
|
+
}
|
|
3259
|
+
return String(body);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
function propagateHeaders(req, addHeaders = {}) {
|
|
3263
|
+
const headers = Object.assign({}, req.headers, addHeaders);
|
|
3264
|
+
headers["x-request-id"] = req.contextId;
|
|
3265
|
+
headers["x-correlation-id"] = req.contextId;
|
|
3266
|
+
headers["x-correlationid"] = req.contextId;
|
|
3267
|
+
delete headers.host;
|
|
3268
|
+
return headers;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
function enrichApplicationJSON(contentType) {
|
|
3272
|
+
const [key] = IEEE754Compatible.split("=");
|
|
3273
|
+
if (!contentType.includes(key)) {
|
|
3274
|
+
contentType += ";" + IEEE754Compatible;
|
|
3275
|
+
}
|
|
3276
|
+
return contentType;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
function isApplicationJSON(contentType) {
|
|
3280
|
+
return contentType && contentType.startsWith("application/json");
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
function isPlainText(contentType) {
|
|
3284
|
+
return contentType && contentType.startsWith("text/plain");
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
function isXML(contentType) {
|
|
3288
|
+
return (
|
|
3289
|
+
contentType &&
|
|
3290
|
+
(contentType.startsWith("application/xml") ||
|
|
3291
|
+
contentType.startsWith("application/atomsvc+xml") ||
|
|
3292
|
+
contentType.startsWith("text/xml") ||
|
|
3293
|
+
contentType.startsWith("text/html"))
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
function isMultipartMixed(contentType) {
|
|
3298
|
+
return contentType && contentType.replace(/\s/g, "").startsWith("multipart/mixed;boundary=");
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
function isMultipartFormData(contentType) {
|
|
3302
|
+
return contentType && contentType.replace(/\s/g, "").startsWith("multipart/form-data;boundary=");
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
function encodeURIKey(value) {
|
|
3306
|
+
return encodeURIComponent(value).replace(/[/]/g, "%2F").replace(/'/g, "''").replace(/%3A/g, ":");
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function decodeURIKey(value) {
|
|
3310
|
+
return decodeURIComponent(value).replace(/%2F/g, "/");
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
function targetUrl(req) {
|
|
3314
|
+
// Non-batch scenario only
|
|
3315
|
+
let path = req.originalUrl;
|
|
3316
|
+
Object.entries(pathRewrite).forEach(([key, value]) => {
|
|
3317
|
+
path = path.replace(new RegExp(key, "g"), value);
|
|
3318
|
+
});
|
|
3319
|
+
return path;
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
function lookupReturnDefinition(returns, req) {
|
|
3323
|
+
returns = (returns && returns.items) || returns;
|
|
3324
|
+
if (returns && returns.type) {
|
|
3325
|
+
const definition = lookupDefinition(returns.type, req);
|
|
3326
|
+
return definition || lookupReturnPrimitiveDefinition(returns);
|
|
3327
|
+
}
|
|
3328
|
+
return returns;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
function lookupReturnPrimitiveDefinition(returns) {
|
|
3332
|
+
returns = (returns && returns.items) || returns;
|
|
3333
|
+
return {
|
|
3334
|
+
name: lookupODataType(returns && returns.type),
|
|
3335
|
+
elements: {
|
|
3336
|
+
value: returns,
|
|
3337
|
+
},
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
function lookupODataType(type) {
|
|
3342
|
+
const odataType = Object.keys(DataTypeOData).find((key) => {
|
|
3343
|
+
return DataTypeOData[key] === type;
|
|
3344
|
+
});
|
|
3345
|
+
if (odataType && odataType.startsWith("_")) {
|
|
3346
|
+
return odataType.substr(1);
|
|
3347
|
+
}
|
|
3348
|
+
return odataType;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
function structureKeys(structure) {
|
|
3352
|
+
const keys = [];
|
|
3353
|
+
for (const key in structure || {}) {
|
|
3354
|
+
keys.push(key);
|
|
3355
|
+
}
|
|
3356
|
+
return keys;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function definitionElements(definition) {
|
|
3360
|
+
if (definition && definition.elements) {
|
|
3361
|
+
return structureKeys(definition.elements).reduce((elements, key) => {
|
|
3362
|
+
const element = definition.elements[key];
|
|
3363
|
+
if (element["@cds.api.ignore"]) {
|
|
3364
|
+
return elements;
|
|
3365
|
+
}
|
|
3366
|
+
elements[key] = element;
|
|
3367
|
+
if (
|
|
3368
|
+
(element.type === "cds.Composition" || element.type === "cds.Association") &&
|
|
3369
|
+
element.keys &&
|
|
3370
|
+
element._target
|
|
3371
|
+
) {
|
|
3372
|
+
element.keys.forEach((key) => {
|
|
3373
|
+
const targetKey = key.ref[0];
|
|
3374
|
+
const foreignKey = `${element.name}_${targetKey}`;
|
|
3375
|
+
if (!elements[foreignKey]) {
|
|
3376
|
+
elements[foreignKey] = {
|
|
3377
|
+
key: element.key,
|
|
3378
|
+
type: element._target.elements[targetKey].type,
|
|
3379
|
+
name: foreignKey,
|
|
3380
|
+
parent: element.parent,
|
|
3381
|
+
kind: element.kind,
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
return elements;
|
|
3387
|
+
}, {});
|
|
3388
|
+
}
|
|
3389
|
+
return {};
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
function definitionKeys(definition) {
|
|
3393
|
+
const elements = definitionElements(definition);
|
|
3394
|
+
return structureKeys(elements).reduce((keys, key) => {
|
|
3395
|
+
if (elements[key].key) {
|
|
3396
|
+
keys[key] = elements[key];
|
|
3397
|
+
}
|
|
3398
|
+
return keys;
|
|
3399
|
+
}, {});
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
function elementType(element, req) {
|
|
3403
|
+
let type;
|
|
3404
|
+
if (element) {
|
|
3405
|
+
type = element.type;
|
|
3406
|
+
if (element["@odata.Type"] || element["@odata.type"]) {
|
|
3407
|
+
const odataType = localName(element["@odata.Type"] || element["@odata.type"]);
|
|
3408
|
+
if (odataType && DataTypeOData[odataType]) {
|
|
3409
|
+
type = DataTypeOData[odataType];
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
if (!type && element.items && element.items.type) {
|
|
3413
|
+
type = element.items.type;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
return baseElementType(type, req.csn);
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
function baseElementType(type, csn) {
|
|
3420
|
+
if (type && csn.definitions[type]) {
|
|
3421
|
+
type = csn.definitions[type].type;
|
|
3422
|
+
type = baseElementType(type, csn);
|
|
3423
|
+
}
|
|
3424
|
+
return type;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
function findElementByType(elements, type, req) {
|
|
3428
|
+
return structureKeys(elements)
|
|
3429
|
+
.filter((key) => {
|
|
3430
|
+
return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
|
|
3431
|
+
})
|
|
3432
|
+
.find((key) => {
|
|
3433
|
+
const element = elements[key];
|
|
3434
|
+
return element && elementType(element, req) === type;
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
function findElementByAnnotation(elements, annotation) {
|
|
3439
|
+
return structureKeys(elements)
|
|
3440
|
+
.filter((key) => {
|
|
3441
|
+
return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
|
|
3442
|
+
})
|
|
3443
|
+
.find((key) => {
|
|
3444
|
+
const element = elements[key];
|
|
3445
|
+
return element && !!element[annotation];
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
function findElementValueByAnnotation(elements, annotation) {
|
|
3450
|
+
const elementName = findElementByAnnotation(elements, annotation);
|
|
3451
|
+
if (elementName) {
|
|
3452
|
+
const elementValue = elements[elementName][annotation];
|
|
3453
|
+
if (elementValue) {
|
|
3454
|
+
return elementValue["="] || elementValue;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
return undefined;
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
function findEndingElementName(elements, url) {
|
|
3461
|
+
return structureKeys(elements)
|
|
3462
|
+
.filter((key) => {
|
|
3463
|
+
return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
|
|
3464
|
+
})
|
|
3465
|
+
.find((key) => {
|
|
3466
|
+
const element = elements[key];
|
|
3467
|
+
return url.contextPath.endsWith(`/${element.name}`);
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
function determineLocale(req) {
|
|
3472
|
+
let locale = cds.env.i18n && cds.env.i18n.default_language;
|
|
3473
|
+
try {
|
|
3474
|
+
locale = require("../../req/locale")(req);
|
|
3475
|
+
} catch {
|
|
3476
|
+
try {
|
|
3477
|
+
// CDS 3
|
|
3478
|
+
locale = require("@sap/cds-runtime/lib/cds-services/adapter/utils/locale")({ req }); // eslint-disable-line cds/no-missing-dependencies
|
|
3479
|
+
} catch {
|
|
3480
|
+
// Default
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
if (locale && locale.length >= 2) {
|
|
3484
|
+
locale = locale.substr(0, 2).toLowerCase() + locale.slice(2);
|
|
3485
|
+
}
|
|
3486
|
+
return locale || "en";
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
const ensureArray = (value) => {
|
|
3490
|
+
if (!value) {
|
|
3491
|
+
return [];
|
|
3492
|
+
}
|
|
3493
|
+
if (Array.isArray(value)) {
|
|
3494
|
+
return value;
|
|
3495
|
+
}
|
|
3496
|
+
if (typeof value === "string") {
|
|
3497
|
+
return value.split(",");
|
|
3498
|
+
}
|
|
3499
|
+
if (typeof value === "object") {
|
|
3500
|
+
return Object.keys(value)
|
|
3501
|
+
.filter((k) => value[k])
|
|
3502
|
+
.sort();
|
|
3503
|
+
}
|
|
3504
|
+
return [];
|
|
3505
|
+
};
|
|
3506
|
+
|
|
3507
|
+
function decodeBase64(b64String) {
|
|
3508
|
+
return Buffer.from(b64String, "base64").toString();
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function decodeJwtTokenBody(token) {
|
|
3512
|
+
const parts = token.split(".");
|
|
3513
|
+
if (parts.length > 1) {
|
|
3514
|
+
return JSON.parse(decodeBase64(parts[1]));
|
|
3515
|
+
}
|
|
3516
|
+
throw new Error("Invalid JWT token");
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
function setContentLength(headers, body) {
|
|
3520
|
+
if (body && !(headers["transfer-encoding"] || "").includes("chunked")) {
|
|
3521
|
+
headers["content-length"] = Buffer.byteLength(body);
|
|
3522
|
+
} else {
|
|
3523
|
+
delete headers["content-length"];
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
function processMultipartMixed(
|
|
3528
|
+
req,
|
|
3529
|
+
multiPartBody,
|
|
3530
|
+
contentType,
|
|
3531
|
+
urlProcessor,
|
|
3532
|
+
bodyHeadersProcessor,
|
|
3533
|
+
contentIdOrder = [],
|
|
3534
|
+
direction
|
|
3535
|
+
) {
|
|
3536
|
+
let maxContentId = 1;
|
|
3537
|
+
|
|
3538
|
+
function nextContentID() {
|
|
3539
|
+
return "cov2ap_" + String(maxContentId++);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
if (!multiPartBody || !(typeof multiPartBody === "string")) {
|
|
3543
|
+
const error = new Error("Invalid multipart body");
|
|
3544
|
+
error.statusCode = 400;
|
|
3545
|
+
throw error;
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
if (!contentType || !(typeof contentType === "string")) {
|
|
3549
|
+
const error = new Error("Invalid content type");
|
|
3550
|
+
error.statusCode = 400;
|
|
3551
|
+
throw error;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
const match = contentType.replace(/\s/g, "").match(/^multipart\/mixed;boundary=([^;]*)/i);
|
|
3555
|
+
let boundary = match && match.pop();
|
|
3556
|
+
if (!boundary) {
|
|
3557
|
+
return multiPartBody;
|
|
3558
|
+
}
|
|
3559
|
+
let boundaryChangeSet = "";
|
|
3560
|
+
let urlAfterBlank = false;
|
|
3561
|
+
let bodyAfterBlank = false;
|
|
3562
|
+
let previousLineIsBlank = false;
|
|
3563
|
+
let index = 0;
|
|
3564
|
+
let statusCode;
|
|
3565
|
+
let contentId;
|
|
3566
|
+
let contentIdMisplaced = false;
|
|
3567
|
+
let contentTransferEncoding;
|
|
3568
|
+
let body = "";
|
|
3569
|
+
let headers = {};
|
|
3570
|
+
let method = "";
|
|
3571
|
+
let url = "";
|
|
3572
|
+
const parts = multiPartBody.split("\r\n");
|
|
3573
|
+
const newParts = [];
|
|
3574
|
+
parts.forEach((part) => {
|
|
3575
|
+
const match = part.replace(/\s/g, "").match(/^content-type:multipart\/mixed;boundary=(.*)$/i);
|
|
3576
|
+
if (match) {
|
|
3577
|
+
boundaryChangeSet = match.pop();
|
|
3578
|
+
}
|
|
3579
|
+
if (part.startsWith(`--${boundary}`) || (boundaryChangeSet && part.startsWith(`--${boundaryChangeSet}`))) {
|
|
3580
|
+
// Body & Headers
|
|
3581
|
+
if (bodyAfterBlank) {
|
|
3582
|
+
if (bodyHeadersProcessor) {
|
|
3583
|
+
try {
|
|
3584
|
+
const contentType = normalizeContentType(headers);
|
|
3585
|
+
if (isApplicationJSON(contentType)) {
|
|
3586
|
+
body = (body && JSON.parse(body)) || {};
|
|
3587
|
+
}
|
|
3588
|
+
const result = bodyHeadersProcessor({
|
|
3589
|
+
index,
|
|
3590
|
+
statusCode,
|
|
3591
|
+
contentType,
|
|
3592
|
+
body,
|
|
3593
|
+
headers,
|
|
3594
|
+
method,
|
|
3595
|
+
url,
|
|
3596
|
+
contentId,
|
|
3597
|
+
});
|
|
3598
|
+
body = (result && result.body) || body;
|
|
3599
|
+
headers = (result && result.headers) || headers;
|
|
3600
|
+
} catch (err) {
|
|
3601
|
+
// Error
|
|
3602
|
+
logError(req, "Batch", err);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
if (boundaryChangeSet) {
|
|
3606
|
+
// Inject mandatory content-id for changesets
|
|
3607
|
+
const addContentIdHeader = contentId === undefined || contentIdMisplaced;
|
|
3608
|
+
if (contentId === undefined && direction === ProcessingDirection.Request) {
|
|
3609
|
+
contentId = nextContentID();
|
|
3610
|
+
}
|
|
3611
|
+
if (contentId !== undefined) {
|
|
3612
|
+
if (direction === ProcessingDirection.Request || !contentId.startsWith("cov2ap_")) {
|
|
3613
|
+
if (addContentIdHeader) {
|
|
3614
|
+
// Add content-id to headers of changeset (before url = -3)
|
|
3615
|
+
newParts.splice(-3, 0, `content-id: ${contentId}`);
|
|
3616
|
+
}
|
|
3617
|
+
headers["content-id"] = contentId;
|
|
3618
|
+
}
|
|
3619
|
+
contentIdOrder.push(contentId);
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
// Inject mandatory content-transfer-encoding
|
|
3623
|
+
if (!contentTransferEncoding) {
|
|
3624
|
+
contentTransferEncoding = "binary";
|
|
3625
|
+
// Add content-transfer-encoding to headers (before url = -3)
|
|
3626
|
+
newParts.splice(-3, 0, `content-transfer-encoding: ${contentTransferEncoding}`);
|
|
3627
|
+
}
|
|
3628
|
+
Object.entries(headers).forEach(([name, value]) => {
|
|
3629
|
+
newParts.splice(-1, 0, `${name}: ${value}`);
|
|
3630
|
+
});
|
|
3631
|
+
newParts.push(body);
|
|
3632
|
+
statusCode = undefined;
|
|
3633
|
+
contentId = undefined;
|
|
3634
|
+
contentIdMisplaced = false;
|
|
3635
|
+
contentTransferEncoding = undefined;
|
|
3636
|
+
body = "";
|
|
3637
|
+
headers = {};
|
|
3638
|
+
url = "";
|
|
3639
|
+
index++;
|
|
3640
|
+
}
|
|
3641
|
+
urlAfterBlank = true;
|
|
3642
|
+
bodyAfterBlank = false;
|
|
3643
|
+
newParts.push(part);
|
|
3644
|
+
if (boundaryChangeSet && part === `--${boundaryChangeSet}--`) {
|
|
3645
|
+
boundaryChangeSet = "";
|
|
3646
|
+
}
|
|
3647
|
+
} else if (urlAfterBlank && previousLineIsBlank) {
|
|
3648
|
+
urlAfterBlank = false;
|
|
3649
|
+
bodyAfterBlank = true;
|
|
3650
|
+
// Url
|
|
3651
|
+
const urlParts = part.split(" ");
|
|
3652
|
+
let partMethod = urlParts[0];
|
|
3653
|
+
let partUrl = urlParts.slice(1, -1).join(" ");
|
|
3654
|
+
let partProtocol = urlParts.pop();
|
|
3655
|
+
if (urlProcessor) {
|
|
3656
|
+
const result = urlProcessor({ method: partMethod, url: partUrl });
|
|
3657
|
+
if (result) {
|
|
3658
|
+
partMethod = result.method;
|
|
3659
|
+
partUrl = result.url;
|
|
3660
|
+
part = [partMethod, partUrl, partProtocol].join(" ");
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
method = partMethod;
|
|
3664
|
+
url = partUrl;
|
|
3665
|
+
|
|
3666
|
+
newParts.push(part);
|
|
3667
|
+
if (part.startsWith("HTTP/")) {
|
|
3668
|
+
const statusCodeMatch = part.match(/^HTTP\/[\d.]+\s+(\d{3})\s.*$/i);
|
|
3669
|
+
if (statusCodeMatch) {
|
|
3670
|
+
statusCode = parseInt(statusCodeMatch.pop());
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
} else if (bodyAfterBlank && (previousLineIsBlank || body !== "")) {
|
|
3674
|
+
body = body ? `${body}\r\n${part}` : part;
|
|
3675
|
+
} else if (part !== "") {
|
|
3676
|
+
const partIsContentId = part.toLowerCase().startsWith("content-id:");
|
|
3677
|
+
if (partIsContentId) {
|
|
3678
|
+
const colonIndex = part.indexOf(":");
|
|
3679
|
+
if (colonIndex !== -1) {
|
|
3680
|
+
contentId = part.substr(colonIndex + 1).trim();
|
|
3681
|
+
contentIdMisplaced = !!bodyAfterBlank;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
const partContentTransferEncoding = part.toLowerCase().startsWith("content-transfer-encoding:");
|
|
3685
|
+
if (partContentTransferEncoding && !bodyAfterBlank) {
|
|
3686
|
+
const colonIndex = part.indexOf(":");
|
|
3687
|
+
if (colonIndex !== -1) {
|
|
3688
|
+
contentTransferEncoding = part.substr(colonIndex + 1).trim();
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
if (!bodyAfterBlank) {
|
|
3692
|
+
if (!(direction === ProcessingDirection.Response && partIsContentId && contentId.startsWith("cov2ap_"))) {
|
|
3693
|
+
newParts.push(part);
|
|
3694
|
+
}
|
|
3695
|
+
} else {
|
|
3696
|
+
let colonIndex = part.indexOf(":");
|
|
3697
|
+
if (colonIndex !== -1) {
|
|
3698
|
+
headers[part.substr(0, colonIndex).toLowerCase()] = part.substr(colonIndex + 1).trim();
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
} else {
|
|
3702
|
+
newParts.push(part);
|
|
3703
|
+
}
|
|
3704
|
+
previousLineIsBlank = part === "";
|
|
3705
|
+
});
|
|
3706
|
+
return newParts.join("\r\n");
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
function traceRequest(req, name, method, url, headers, body) {
|
|
3710
|
+
if (LOG._debug) {
|
|
3711
|
+
const _url = url || "";
|
|
3712
|
+
const _headers = JSON.stringify(headers || {});
|
|
3713
|
+
const _body = typeof body === "string" ? body : body ? JSON.stringify(body) : "";
|
|
3714
|
+
trace(req, name, `${method} ${_url}`, _headers && "Headers:", _headers, _body && "Body:", _body);
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
function traceResponse(req, name, statusCode, statusMessage, headers, body) {
|
|
3719
|
+
if (LOG._debug) {
|
|
3720
|
+
const _headers = JSON.stringify(headers || {});
|
|
3721
|
+
const _body = typeof body === "string" ? body : body ? JSON.stringify(body) : "";
|
|
3722
|
+
trace(
|
|
3723
|
+
req,
|
|
3724
|
+
name,
|
|
3725
|
+
`${statusCode || ""} ${statusMessage || ""}`,
|
|
3726
|
+
_headers && "Headers:",
|
|
3727
|
+
_headers,
|
|
3728
|
+
_body && "Body:",
|
|
3729
|
+
_body
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
function trace(req, name, ...lines) {
|
|
3735
|
+
if (LOG._debug) {
|
|
3736
|
+
initCDSContext(req);
|
|
3737
|
+
LOG.debug(name, lines.filter((line) => !!line).join("\n"));
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
function logError(req, name, error) {
|
|
3742
|
+
if (LOG._error) {
|
|
3743
|
+
initCDSContext(req);
|
|
3744
|
+
LOG.error(name, error);
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
function logWarning(req, name, message, info) {
|
|
3749
|
+
if (LOG._warn) {
|
|
3750
|
+
initCDSContext(req);
|
|
3751
|
+
LOG.warn(name, message, info);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
function initCDSContext(req) {
|
|
3756
|
+
cds.context = cds.context || {
|
|
3757
|
+
id: req.contextId,
|
|
3758
|
+
tenant: req.tenant,
|
|
3759
|
+
user: req.user,
|
|
3760
|
+
_: { req, res: req.res },
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
return router;
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
module.exports = cov2ap;
|