@cap-js/ord 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build.js +4 -3
- package/lib/common/register-compile-targets.js +1 -1
- package/lib/configuration.js +161 -0
- package/lib/constants.js +11 -10
- package/lib/defaults.js +73 -42
- package/lib/integration-dependency.js +0 -2
- package/lib/interop-csn.js +1 -1
- package/lib/ord.js +58 -296
- package/lib/services/mtx-ord-provider-service.js +13 -9
- package/lib/services/ord-service.js +73 -21
- package/lib/templates.js +35 -44
- package/package.json +4 -3
- package/lib/utils.js +0 -12
package/lib/build.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
const os = require("os");
|
|
1
2
|
const path = require("path");
|
|
2
3
|
const cds = require("@sap/cds");
|
|
3
4
|
const _ = require("lodash");
|
|
4
5
|
const Piscina = require("piscina");
|
|
5
6
|
const cliProgress = require("cli-progress");
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const defaults = require("./defaults.js");
|
|
8
9
|
const { ord } = require("./index");
|
|
9
10
|
const registerCompileTargets = require("./common/register-compile-targets");
|
|
10
|
-
const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME } = require("./constants");
|
|
11
|
+
const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME, DOCUMENT_PERSPECTIVES } = require("./constants");
|
|
11
12
|
|
|
12
13
|
module.exports = class OrdBuildPlugin extends cds.build.Plugin {
|
|
13
14
|
static taskDefaults = { src: cds.env.folders.srv };
|
|
@@ -53,7 +54,7 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
|
|
|
53
54
|
resourceDefinition.url = this._createRelativePath(resourceDefinition.url);
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
return clone;
|
|
57
|
+
return defaults.adjustForPerspective(clone, DOCUMENT_PERSPECTIVES.SystemVersion);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
_createRelativePath(url) {
|
|
@@ -12,7 +12,7 @@ module.exports = () => {
|
|
|
12
12
|
// registrations. Re-apply so that compilation works accordingly during build and runtime.
|
|
13
13
|
|
|
14
14
|
Object.keys(cds.env.plugins || {})
|
|
15
|
-
.filter((plugin) => plugin
|
|
15
|
+
.filter((plugin) => !!PROTOCOL_PROVIDERS[plugin])
|
|
16
16
|
.forEach((plugin) => {
|
|
17
17
|
try {
|
|
18
18
|
const protocol = PROTOCOL_PROVIDERS[plugin];
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const cds = require("@sap/cds");
|
|
3
|
+
const _ = require("lodash");
|
|
4
|
+
const { readFileSync } = require("fs");
|
|
5
|
+
|
|
6
|
+
const Logger = require("./logger");
|
|
7
|
+
const { getRFC3339Date } = require("./date");
|
|
8
|
+
const defaults = require("./defaults");
|
|
9
|
+
const { createEntityTypeMappingsItemTemplate } = require("./templates");
|
|
10
|
+
const { CONTENT_MERGE_KEY, CDS_ELEMENT_KIND } = require("./constants");
|
|
11
|
+
|
|
12
|
+
module.exports = class Configuration {
|
|
13
|
+
constructor(csn) {
|
|
14
|
+
const env = cds.env["ord"];
|
|
15
|
+
const packageName = this._loadNameFromPackageJson();
|
|
16
|
+
const eventApplicationNamespace = cds.env?.export?.asyncapi?.applicationNamespace;
|
|
17
|
+
const ordNamespace = cds.env["ord"]?.namespace || `customer.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
18
|
+
const internalNamespace = env?.internalNamespace;
|
|
19
|
+
|
|
20
|
+
if (eventApplicationNamespace && ordNamespace !== eventApplicationNamespace) {
|
|
21
|
+
Logger.warn("ORD and AsyncAPI namespaces should be the same.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this._env = env;
|
|
25
|
+
this._packageName = packageName;
|
|
26
|
+
this._ordNamespace = ordNamespace;
|
|
27
|
+
this._internalNamespace = internalNamespace;
|
|
28
|
+
this._lastUpdate = getRFC3339Date();
|
|
29
|
+
this._serviceNames = this._resolveServiceNames(csn);
|
|
30
|
+
this._existingProductORDId = env?.existingProductORDId;
|
|
31
|
+
this._apiResourceNames = this._resolveApiResourceNames(csn);
|
|
32
|
+
this._entityTypeTargets = this._resolveEntityTypeTargets(csn);
|
|
33
|
+
this._eventServiceNames = this._resolveEventServiceNames(csn);
|
|
34
|
+
this._appName = packageName.replace(/^@/, "").replace(/[@/]/g, "-");
|
|
35
|
+
this._policyLevels = env?.policyLevels || (env?.policyLevel && [env?.policyLevel]) || defaults.policyLevels;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get env() {
|
|
39
|
+
return this._env;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get appName() {
|
|
43
|
+
return this._appName;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get lastUpdate() {
|
|
47
|
+
return this._lastUpdate;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get packageName() {
|
|
51
|
+
return this._packageName;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get serviceNames() {
|
|
55
|
+
return this._serviceNames;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get ordNamespace() {
|
|
59
|
+
return this._ordNamespace;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get internalNamespace() {
|
|
63
|
+
return this._internalNamespace;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get policyLevels() {
|
|
67
|
+
return this._policyLevels;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get apiResourceNames() {
|
|
71
|
+
return this._apiResourceNames;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get entityTypeTargets() {
|
|
75
|
+
return this._entityTypeTargets;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get eventServiceNames() {
|
|
79
|
+
return this._eventServiceNames;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get hasSAPPolicyLevel() {
|
|
83
|
+
return this.policyLevels.some((policyLevel) => policyLevel.split(":")[0].toLowerCase() === "sap");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get existingProductORDId() {
|
|
87
|
+
return this._existingProductORDId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_resolveServiceNames(csn) {
|
|
91
|
+
return Object.keys(csn.definitions) //
|
|
92
|
+
.filter((name) => this._isValidService(name, csn.definitions[name]));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_loadNameFromPackageJson() {
|
|
96
|
+
const packageJsonPath = path.join(cds.root, "package.json");
|
|
97
|
+
|
|
98
|
+
if (!cds.utils.exists(packageJsonPath)) {
|
|
99
|
+
throw new Error("package.json not found in the project root directory");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf-8")).name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_isBlockedServiceName(name) {
|
|
106
|
+
return ["cds.xt.MTXServices", "MtxOrdProviderService", "OpenResourceDiscoveryService"] //
|
|
107
|
+
.some((blocked) => name.includes(blocked));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_resolveApiResourceNames(csn) {
|
|
111
|
+
return Object.keys(csn.definitions)
|
|
112
|
+
.filter((name) => this._isValidService(name, csn.definitions[name]))
|
|
113
|
+
.filter((name) => !this._serviceOnlyContainsEvents(csn.definitions[name]));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_resolveEntityTypeTargets(csn) {
|
|
117
|
+
return _.uniqBy(
|
|
118
|
+
Object.keys(csn.definitions)
|
|
119
|
+
.filter((name) => !name.includes(".texts"))
|
|
120
|
+
.filter((name) => !this._isBlockedServiceName(name))
|
|
121
|
+
.filter((name) => csn.definitions[name].kind === CDS_ELEMENT_KIND.entity)
|
|
122
|
+
.filter((name) => csn.definitions[name]["_service"]?.["@protocol"] !== "none")
|
|
123
|
+
.flatMap((name) => createEntityTypeMappingsItemTemplate(csn.definitions[name]) || []),
|
|
124
|
+
CONTENT_MERGE_KEY,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_resolveEventServiceNames(csn) {
|
|
129
|
+
const serviceNames = this._resolveServiceNames(csn);
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
...new Set(
|
|
133
|
+
Object.keys(csn.definitions)
|
|
134
|
+
.filter((name) => !this._isBlockedServiceName(name))
|
|
135
|
+
.filter((name) => csn.definitions[name].kind === CDS_ELEMENT_KIND.event)
|
|
136
|
+
.filter((name) => csn.definitions[name]["_service"]?.["@protocol"] !== "none")
|
|
137
|
+
.filter((name) => serviceNames.some((serviceName) => name.startsWith(`${serviceName}.`)))
|
|
138
|
+
.map((name) => serviceNames.find((serviceName) => name.startsWith(`${serviceName}.`))),
|
|
139
|
+
),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_isValidService(key, definition) {
|
|
144
|
+
const isExternalService = Object.keys(cds).includes("requires") && Object.keys(cds.requires).includes(key);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
definition.kind === CDS_ELEMENT_KIND.service &&
|
|
148
|
+
!definition["@cds.external"] &&
|
|
149
|
+
definition["@protocol"] !== "none" &&
|
|
150
|
+
!isExternalService &&
|
|
151
|
+
!this._isBlockedServiceName(key)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_serviceOnlyContainsEvents(definition) {
|
|
156
|
+
return (
|
|
157
|
+
Object.keys(definition.events || {}).length > 0 &&
|
|
158
|
+
["actions", "entities", "functions"].every((key) => Object.keys(definition[key] || {}).length === 0)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
};
|
package/lib/constants.js
CHANGED
|
@@ -8,11 +8,6 @@ const BASIC_AUTH_HEADER_KEY = "authorization";
|
|
|
8
8
|
|
|
9
9
|
const BUILD_DEFAULT_PATH = "gen/ord";
|
|
10
10
|
|
|
11
|
-
const BLOCKED_SERVICE_NAME = Object.freeze({
|
|
12
|
-
MTXServices: "cds.xt.MTXServices",
|
|
13
|
-
OpenResourceDiscoveryService: "OpenResourceDiscoveryService",
|
|
14
|
-
});
|
|
15
|
-
|
|
16
11
|
const ORD_ACCESS_STRATEGY = Object.freeze({
|
|
17
12
|
Open: "open",
|
|
18
13
|
Basic: "basic-auth",
|
|
@@ -76,8 +71,6 @@ const LEVEL = Object.freeze({
|
|
|
76
71
|
subEntity: "sub-entity",
|
|
77
72
|
});
|
|
78
73
|
|
|
79
|
-
const OPEN_RESOURCE_DISCOVERY_VERSION = "1.12";
|
|
80
|
-
|
|
81
74
|
const OPENAPI_SERVERS_ANNOTATION = "@OpenAPI.servers";
|
|
82
75
|
|
|
83
76
|
const ORD_EXTENSIONS_PREFIX = "@ORD.Extensions.";
|
|
@@ -89,8 +82,9 @@ const ORD_EXISTING_PRODUCT_PROPERTY = "existingProductORDId";
|
|
|
89
82
|
const ORD_RESOURCE_TYPE = Object.freeze({
|
|
90
83
|
api: "api",
|
|
91
84
|
event: "event",
|
|
92
|
-
integrationDependency: "integrationDependency",
|
|
93
85
|
entityType: "entityType",
|
|
86
|
+
dataProduct: "dataProduct",
|
|
87
|
+
integrationDependency: "integrationDependency",
|
|
94
88
|
});
|
|
95
89
|
|
|
96
90
|
const ORD_SERVICE_NAME = "OpenResourceDiscoveryService";
|
|
@@ -181,12 +175,18 @@ const AUTH_STRINGS = Object.freeze({
|
|
|
181
175
|
WWW_AUTHENTICATE_REALM: 'Basic realm="401"',
|
|
182
176
|
});
|
|
183
177
|
|
|
178
|
+
const DOCUMENT_PERSPECTIVES = Object.freeze({
|
|
179
|
+
SystemVersion: "system-version",
|
|
180
|
+
SystemInstance: "system-instance",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const LOCAL_TENANT_ID_HEADER_KEY = "local-tenant-id";
|
|
184
|
+
|
|
184
185
|
module.exports = {
|
|
185
186
|
AUTHENTICATION_TYPE,
|
|
186
187
|
AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,
|
|
187
188
|
BASIC_AUTH_HEADER_KEY,
|
|
188
189
|
BUILD_DEFAULT_PATH,
|
|
189
|
-
BLOCKED_SERVICE_NAME,
|
|
190
190
|
CAP_TO_ORD_PROTOCOL_MAP,
|
|
191
191
|
CDS_ELEMENT_KIND,
|
|
192
192
|
CF_MTLS_HEADERS,
|
|
@@ -202,7 +202,6 @@ module.exports = {
|
|
|
202
202
|
INTEGRATION_DEPENDENCY_RESOURCE_NAME,
|
|
203
203
|
LEVEL,
|
|
204
204
|
MCP_RESOURCE_DEFINITION_TYPE,
|
|
205
|
-
OPEN_RESOURCE_DISCOVERY_VERSION,
|
|
206
205
|
OPENAPI_SERVERS_ANNOTATION,
|
|
207
206
|
ORD_ACCESS_STRATEGY,
|
|
208
207
|
ORD_API_PROTOCOL,
|
|
@@ -223,4 +222,6 @@ module.exports = {
|
|
|
223
222
|
CF_MTLS_ERROR_REASON,
|
|
224
223
|
HTTP_CONFIG,
|
|
225
224
|
AUTH_STRINGS,
|
|
225
|
+
DOCUMENT_PERSPECTIVES,
|
|
226
|
+
LOCAL_TENANT_ID_HEADER_KEY,
|
|
226
227
|
};
|
package/lib/defaults.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
const
|
|
2
|
-
const { hasSAPPolicyLevel } = require("./utils");
|
|
1
|
+
const fs = require("fs");
|
|
3
2
|
const _ = require("lodash");
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
const { join } = require("path");
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
];
|
|
6
|
+
const {
|
|
7
|
+
ORD_RESOURCE_TYPE,
|
|
8
|
+
SHORT_DESCRIPTION_PREFIX,
|
|
9
|
+
RESOURCE_VISIBILITY,
|
|
10
|
+
DOCUMENT_PERSPECTIVES,
|
|
11
|
+
CONTENT_MERGE_KEY,
|
|
12
|
+
} = require("./constants");
|
|
13
13
|
|
|
14
14
|
const stringTypeCheck = (value) => typeof value === "string";
|
|
15
15
|
const arrayTypeCheck = (value) => Array.isArray(value);
|
|
@@ -77,8 +77,9 @@ const mapEnvPackageInfo = (packageConfig) => {
|
|
|
77
77
|
*/
|
|
78
78
|
module.exports = {
|
|
79
79
|
$schema: "https://open-resource-discovery.github.io/specification/spec-v1/interfaces/Document.schema.json",
|
|
80
|
-
openResourceDiscovery:
|
|
80
|
+
openResourceDiscovery: "1.14",
|
|
81
81
|
policyLevels: ["none"],
|
|
82
|
+
groupTypeId: "sap.cds:service",
|
|
82
83
|
description: "this is an application description",
|
|
83
84
|
products: (name) => [
|
|
84
85
|
{
|
|
@@ -88,40 +89,38 @@ module.exports = {
|
|
|
88
89
|
vendor: "customer:vendor:Customer:",
|
|
89
90
|
},
|
|
90
91
|
],
|
|
91
|
-
|
|
92
|
-
packages: function getPackageData(appConfig) {
|
|
92
|
+
packages: (appConfig, products) => {
|
|
93
93
|
const name = appConfig.appName;
|
|
94
94
|
const ordNamespace = appConfig.ordNamespace;
|
|
95
|
-
const productsOrdId = appConfig.existingProductORDId ||
|
|
95
|
+
const productsOrdId = appConfig.existingProductORDId || products?.[0]?.ordId;
|
|
96
96
|
const { vendor, ...envValues } = mapEnvPackageInfo(appConfig?.env?.packages?.[0]);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
ordId: generateUniquePackageOrdId(ordNamespace, name, "", RESOURCE_VISIBILITY.public),
|
|
97
|
+
const visibilities = !appConfig.hasSAPPolicyLevel
|
|
98
|
+
? [RESOURCE_VISIBILITY.public]
|
|
99
|
+
: Object.values(RESOURCE_VISIBILITY);
|
|
100
|
+
const packageTypes = !appConfig.hasSAPPolicyLevel
|
|
101
|
+
? [{ tag: "", type: "General" }]
|
|
102
|
+
: [
|
|
103
|
+
{ tag: `-${ORD_RESOURCE_TYPE.api}`, type: "APIs" },
|
|
104
|
+
{ tag: `-${ORD_RESOURCE_TYPE.event}`, type: "Events" },
|
|
105
|
+
{ tag: `-${ORD_RESOURCE_TYPE.entityType}`, type: "Entity Types" },
|
|
106
|
+
{ tag: `-${ORD_RESOURCE_TYPE.dataProduct}`, type: "Data Products" },
|
|
107
|
+
{ tag: `-${ORD_RESOURCE_TYPE.integrationDependency}`, type: "Integration Dependencies" },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
return _.uniqBy(
|
|
111
|
+
packageTypes.flatMap(({ tag, type }) =>
|
|
112
|
+
visibilities.map((visibility) => ({
|
|
113
|
+
ordId: generateUniquePackageOrdId(ordNamespace, name, tag, visibility),
|
|
116
114
|
title: nameWithSpaces(name),
|
|
117
|
-
...generatePackageDescriptions(name,
|
|
115
|
+
...generatePackageDescriptions(name, type, visibility),
|
|
118
116
|
version: "1.0.0",
|
|
119
117
|
...(productsOrdId && { partOfProducts: [productsOrdId || defaultProductOrdId(name)] }),
|
|
120
118
|
vendor: vendor || "customer:vendor:Customer:",
|
|
121
119
|
...envValues,
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
})),
|
|
121
|
+
),
|
|
122
|
+
CONTENT_MERGE_KEY,
|
|
123
|
+
);
|
|
125
124
|
},
|
|
126
125
|
consumptionBundles: (appConfig) => [
|
|
127
126
|
{
|
|
@@ -134,23 +133,55 @@ module.exports = {
|
|
|
134
133
|
"This Consumption Bundle contains all resources of the reference app which are unprotected and do not require authentication",
|
|
135
134
|
},
|
|
136
135
|
],
|
|
137
|
-
|
|
138
|
-
apiResources: [],
|
|
139
|
-
eventResources: [],
|
|
140
|
-
entityTypes: [],
|
|
141
|
-
baseTemplate: (authConfig) => {
|
|
136
|
+
baseTemplate: (authConfig, tenant) => {
|
|
142
137
|
// Get access strategies from the provided authConfig
|
|
143
138
|
// If auth config is not available, fall back to empty array
|
|
139
|
+
const toggles = cds.env.requires.toggles;
|
|
140
|
+
const extensibility = cds.env.requires.extensibility;
|
|
144
141
|
const accessStrategies = authConfig?.accessStrategies || [];
|
|
142
|
+
|
|
145
143
|
return {
|
|
146
144
|
openResourceDiscoveryV1: {
|
|
147
145
|
documents: [
|
|
148
146
|
{
|
|
149
147
|
url: "/ord/v1/documents/ord-document",
|
|
148
|
+
perspective: DOCUMENT_PERSPECTIVES.SystemVersion,
|
|
150
149
|
accessStrategies,
|
|
151
150
|
},
|
|
151
|
+
...(!tenant || !(toggles || extensibility)
|
|
152
|
+
? []
|
|
153
|
+
: [
|
|
154
|
+
{
|
|
155
|
+
url: `/ord/v1/documents/ord-document?perspective=${encodeURIComponent(DOCUMENT_PERSPECTIVES.SystemInstance)}`,
|
|
156
|
+
perspective: DOCUMENT_PERSPECTIVES.SystemInstance,
|
|
157
|
+
accessStrategies,
|
|
158
|
+
},
|
|
159
|
+
]),
|
|
152
160
|
],
|
|
153
161
|
},
|
|
154
162
|
};
|
|
155
163
|
},
|
|
164
|
+
adjustForPerspective: (document, perspective) => {
|
|
165
|
+
document.perspective = perspective;
|
|
166
|
+
|
|
167
|
+
if (perspective === DOCUMENT_PERSPECTIVES.SystemVersion) {
|
|
168
|
+
document.describedSystemVersion = cds.env["ord"]?.describedSystemVersion ?? {
|
|
169
|
+
version: JSON.parse(fs.readFileSync(join(cds.root, "package.json"), "utf-8")).version,
|
|
170
|
+
};
|
|
171
|
+
} else if (perspective === DOCUMENT_PERSPECTIVES.SystemInstance) {
|
|
172
|
+
(document.apiResources || []).forEach((apiResource) => {
|
|
173
|
+
(apiResource.resourceDefinitions || []).forEach((apiResourceDefinition) => {
|
|
174
|
+
apiResourceDefinition.url += `?perspective=${encodeURIComponent(perspective)}`;
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
(document.eventResources || []).forEach((eventResource) => {
|
|
179
|
+
(eventResource.resourceDefinitions || []).forEach((eventResourceDefinition) => {
|
|
180
|
+
eventResourceDefinition.url += `?perspective=${encodeURIComponent(perspective)}`;
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return document;
|
|
186
|
+
},
|
|
156
187
|
};
|
package/lib/interop-csn.js
CHANGED
|
@@ -54,7 +54,7 @@ function add_i18n_texts(csn) {
|
|
|
54
54
|
if (n.startsWith("@")) {
|
|
55
55
|
let annoVal = o[n];
|
|
56
56
|
if (typeof annoVal === "string" && annoVal.startsWith("{i18n>")) {
|
|
57
|
-
let [, x] = annoVal.match(/^\{i18n>(.*)
|
|
57
|
+
let [, x] = annoVal.match(/^\{i18n>(.*)}$/) || [];
|
|
58
58
|
if (x) keys.add(x);
|
|
59
59
|
}
|
|
60
60
|
}
|
package/lib/ord.js
CHANGED
|
@@ -1,221 +1,22 @@
|
|
|
1
|
+
const _ = require("lodash");
|
|
2
|
+
const cds = require("@sap/cds");
|
|
3
|
+
|
|
4
|
+
const Logger = require("./logger");
|
|
5
|
+
const defaults = require("./defaults");
|
|
6
|
+
const { createAuthConfig } = require("./auth/authentication");
|
|
7
|
+
const Configuration = require("./configuration");
|
|
8
|
+
const { getIntegrationDependencies } = require("./integration-dependency");
|
|
1
9
|
const {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
6
|
-
ORD_ODM_ENTITY_NAME_ANNOTATION,
|
|
7
|
-
} = require("./constants");
|
|
10
|
+
getCustomORDContent,
|
|
11
|
+
compareAndHandleCustomORDContentWithExistingContent,
|
|
12
|
+
} = require("./extend-ord-with-custom");
|
|
8
13
|
const {
|
|
9
14
|
createAPIResourceTemplate,
|
|
10
15
|
createEntityTypeTemplate,
|
|
11
|
-
createEntityTypeMappingsItemTemplate,
|
|
12
16
|
createEventResourceTemplate,
|
|
13
17
|
createGroupsTemplateForService,
|
|
14
18
|
_propagateORDVisibility,
|
|
15
19
|
} = require("./templates");
|
|
16
|
-
const { getIntegrationDependencies } = require("./integration-dependency");
|
|
17
|
-
const {
|
|
18
|
-
getCustomORDContent,
|
|
19
|
-
compareAndHandleCustomORDContentWithExistingContent,
|
|
20
|
-
} = require("./extend-ord-with-custom");
|
|
21
|
-
const { getRFC3339Date } = require("./date");
|
|
22
|
-
const { createAuthConfig } = require("./auth/authentication");
|
|
23
|
-
|
|
24
|
-
const Logger = require("./logger");
|
|
25
|
-
const _ = require("lodash");
|
|
26
|
-
const cds = require("@sap/cds");
|
|
27
|
-
const defaults = require("./defaults");
|
|
28
|
-
const path = require("path");
|
|
29
|
-
|
|
30
|
-
const initializeAppConfig = (csn) => {
|
|
31
|
-
const packageJson = _loadPackageJson();
|
|
32
|
-
const packageName = packageJson.name;
|
|
33
|
-
const appName = _formatAppName(packageName);
|
|
34
|
-
const lastUpdate = getRFC3339Date();
|
|
35
|
-
|
|
36
|
-
const ordNamespace = _getORDNamespace(packageName);
|
|
37
|
-
const eventApplicationNamespace = cds.env?.export?.asyncapi?.applicationNamespace;
|
|
38
|
-
|
|
39
|
-
_validateNamespaces(ordNamespace, eventApplicationNamespace);
|
|
40
|
-
|
|
41
|
-
const { serviceNames, apiResourceNames, apiEndpoints, eventServiceNames, entityTypeTargets } =
|
|
42
|
-
_triageCsnDefinitions(csn);
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
env: cds.env["ord"],
|
|
46
|
-
lastUpdate,
|
|
47
|
-
appName,
|
|
48
|
-
apiEndpoints: Array.from(apiEndpoints),
|
|
49
|
-
eventServiceNames,
|
|
50
|
-
serviceNames,
|
|
51
|
-
apiResourceNames,
|
|
52
|
-
entityTypeTargets: _.uniqBy(entityTypeTargets, CONTENT_MERGE_KEY),
|
|
53
|
-
ordNamespace,
|
|
54
|
-
eventApplicationNamespace,
|
|
55
|
-
packageName,
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
function _loadPackageJson() {
|
|
60
|
-
const packageJsonPath = path.join(cds.root, "package.json");
|
|
61
|
-
if (!cds.utils.exists(packageJsonPath)) {
|
|
62
|
-
throw new Error(`package.json not found in the project root directory`);
|
|
63
|
-
}
|
|
64
|
-
return require(packageJsonPath);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function _formatAppName(packageName) {
|
|
68
|
-
return packageName.replace(/^[@]/, "").replace(/[@/]/g, "-");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function _getORDNamespace(packageName) {
|
|
72
|
-
const vendorNamespace = "customer";
|
|
73
|
-
return cds.env["ord"]?.namespace || `${vendorNamespace}.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function _validateNamespaces(ordNamespace, eventApplicationNamespace) {
|
|
77
|
-
if (eventApplicationNamespace && ordNamespace !== eventApplicationNamespace) {
|
|
78
|
-
Logger.warn("ORD and AsyncAPI namespaces should be the same.");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function _triageCsnDefinitions(csn) {
|
|
83
|
-
const pendingApiResourceNames = [];
|
|
84
|
-
const apiEndpoints = new Set();
|
|
85
|
-
const pendingEventServiceNames = new Set();
|
|
86
|
-
const entityTypeTargets = [];
|
|
87
|
-
const serviceNames = Object.keys(csn.definitions).filter((key) => _isValidService(key, csn.definitions[key]));
|
|
88
|
-
|
|
89
|
-
for (const definitionKey of Object.keys(csn.definitions)) {
|
|
90
|
-
const definitionObj = csn.definitions[definitionKey];
|
|
91
|
-
if (
|
|
92
|
-
definitionKey.includes(BLOCKED_SERVICE_NAME.MTXServices) ||
|
|
93
|
-
definitionKey.includes(BLOCKED_SERVICE_NAME.OpenResourceDiscoveryService)
|
|
94
|
-
) {
|
|
95
|
-
Logger.warn("ORD service name", definitionKey, "is blocked.");
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
switch (definitionObj.kind) {
|
|
99
|
-
case CDS_ELEMENT_KIND.service: {
|
|
100
|
-
const apiResourceName = _handleApiResource(definitionKey, definitionObj);
|
|
101
|
-
if (apiResourceName) pendingApiResourceNames.push(apiResourceName);
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
case CDS_ELEMENT_KIND.entity: {
|
|
105
|
-
const result = _handleEntity(definitionKey, definitionObj);
|
|
106
|
-
if (result) {
|
|
107
|
-
if (result.apiEndpoint) apiEndpoints.add(result.apiEndpoint);
|
|
108
|
-
if (result.entityTypeTarget) entityTypeTargets.push(...result.entityTypeTarget);
|
|
109
|
-
}
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
case CDS_ELEMENT_KIND.event: {
|
|
113
|
-
const event = _handleEvent(serviceNames, definitionKey, definitionObj);
|
|
114
|
-
if (event) pendingEventServiceNames.add(event);
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
case CDS_ELEMENT_KIND.action:
|
|
118
|
-
case CDS_ELEMENT_KIND.function: {
|
|
119
|
-
const apiEndpoint = _handleActionOrFunction(definitionKey, definitionObj);
|
|
120
|
-
if (apiEndpoint) apiEndpoints.add(apiEndpoint);
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
serviceNames,
|
|
128
|
-
apiResourceNames: pendingApiResourceNames,
|
|
129
|
-
apiEndpoints: Array.from(apiEndpoints),
|
|
130
|
-
eventServiceNames: [...pendingEventServiceNames],
|
|
131
|
-
entityTypeTargets,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function _handleApiResource(apiResourceName, serviceDefinition) {
|
|
136
|
-
if (
|
|
137
|
-
_shouldSkipIfServiceOnlyContainsEvents(serviceDefinition) ||
|
|
138
|
-
!_isValidService(apiResourceName, serviceDefinition)
|
|
139
|
-
) {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
return apiResourceName;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function _shouldSkipIfServiceOnlyContainsEvents(serviceDefinition) {
|
|
146
|
-
const isActionsNotContained = !serviceDefinition.actions || Object.keys(serviceDefinition.actions).length === 0;
|
|
147
|
-
const isFunctionsNotContained =
|
|
148
|
-
!serviceDefinition.functions || Object.keys(serviceDefinition.functions).length === 0;
|
|
149
|
-
const isEntitiesNotContained = !serviceDefinition.entities || Object.keys(serviceDefinition.entities).length === 0;
|
|
150
|
-
const isEventsContained = serviceDefinition.events && Object.keys(serviceDefinition.events).length > 0;
|
|
151
|
-
if (isActionsNotContained && isFunctionsNotContained && isEntitiesNotContained && isEventsContained) {
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function _shouldNotSkipIfServiceProtocolIsNone(keyDefinition) {
|
|
158
|
-
if (keyDefinition["_service"] && keyDefinition["_service"]["@protocol"] === "none") {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function _isBlockedServiceName(key) {
|
|
165
|
-
const blockedServices = [BLOCKED_SERVICE_NAME.MTXServices, BLOCKED_SERVICE_NAME.OpenResourceDiscoveryService];
|
|
166
|
-
return blockedServices.some((blocked) => key.includes(blocked));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function _isValidService(key, definition) {
|
|
170
|
-
const isExternalService = Object.keys(cds).includes("requires") ? Object.keys(cds.requires).includes(key) : false;
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
definition.kind === CDS_ELEMENT_KIND.service &&
|
|
174
|
-
!definition["@cds.external"] &&
|
|
175
|
-
definition["@protocol"] !== "none" &&
|
|
176
|
-
!isExternalService &&
|
|
177
|
-
!_isBlockedServiceName(key)
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function _handleEntity(key, keyDefinition) {
|
|
182
|
-
if (!key.includes(".texts") && _shouldNotSkipIfServiceProtocolIsNone(keyDefinition)) {
|
|
183
|
-
const apiEndpoint = key;
|
|
184
|
-
let entityTypeTarget = null;
|
|
185
|
-
if (keyDefinition[ORD_ODM_ENTITY_NAME_ANNOTATION] || keyDefinition[ENTITY_RELATIONSHIP_ANNOTATION]) {
|
|
186
|
-
const mapping = createEntityTypeMappingsItemTemplate(keyDefinition);
|
|
187
|
-
if (Array.isArray(mapping)) entityTypeTarget = mapping;
|
|
188
|
-
else if (mapping) entityTypeTarget = [mapping];
|
|
189
|
-
}
|
|
190
|
-
return { apiEndpoint, entityTypeTarget };
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function _handleEvent(serviceNames, key, keyDefinition) {
|
|
196
|
-
if (_shouldNotSkipIfServiceProtocolIsNone(keyDefinition)) {
|
|
197
|
-
for (const serviceName of serviceNames) {
|
|
198
|
-
if (key.startsWith(serviceName + ".")) {
|
|
199
|
-
return serviceName;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function _handleActionOrFunction(key, keyDefinition) {
|
|
207
|
-
if (_shouldNotSkipIfServiceProtocolIsNone(keyDefinition)) {
|
|
208
|
-
return key;
|
|
209
|
-
}
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const _getPolicyLevels = (appConfig) =>
|
|
214
|
-
appConfig.env?.policyLevels ||
|
|
215
|
-
(appConfig.env?.policyLevel && [appConfig.env?.policyLevel]) ||
|
|
216
|
-
defaults.policyLevels;
|
|
217
|
-
|
|
218
|
-
const _getDescription = (appConfig) => appConfig.env?.description || defaults.description;
|
|
219
20
|
|
|
220
21
|
const _getGroups = (csn, appConfig) => {
|
|
221
22
|
return appConfig.serviceNames
|
|
@@ -223,18 +24,8 @@ const _getGroups = (csn, appConfig) => {
|
|
|
223
24
|
.filter((resource) => !!resource);
|
|
224
25
|
};
|
|
225
26
|
|
|
226
|
-
const _getPackages = (appConfig) => {
|
|
227
|
-
return defaults.packages(appConfig);
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const _getEntityTypes = (appConfig, packageIds) => {
|
|
231
|
-
if (!appConfig.entityTypeTargets?.length) return [];
|
|
232
|
-
|
|
233
|
-
return appConfig.entityTypeTargets.flatMap((entity) => createEntityTypeTemplate(appConfig, packageIds, entity));
|
|
234
|
-
};
|
|
235
|
-
|
|
236
27
|
const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
|
|
237
|
-
|
|
28
|
+
return appConfig.apiResourceNames.flatMap((apiResourceName) =>
|
|
238
29
|
createAPIResourceTemplate(
|
|
239
30
|
apiResourceName,
|
|
240
31
|
csn.definitions[apiResourceName],
|
|
@@ -243,8 +34,6 @@ const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
|
|
|
243
34
|
accessStrategies,
|
|
244
35
|
),
|
|
245
36
|
);
|
|
246
|
-
|
|
247
|
-
return apiResources;
|
|
248
37
|
};
|
|
249
38
|
|
|
250
39
|
const _getEventResources = (csn, appConfig, packageIds, accessStrategies) => {
|
|
@@ -253,16 +42,9 @@ const _getEventResources = (csn, appConfig, packageIds, accessStrategies) => {
|
|
|
253
42
|
);
|
|
254
43
|
};
|
|
255
44
|
|
|
256
|
-
function _getOpenResourceDiscovery(appConfig) {
|
|
257
|
-
return appConfig.env?.openResourceDiscovery || defaults.openResourceDiscovery;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function _getConsumptionBundles(appConfig) {
|
|
261
|
-
return appConfig.env?.consumptionBundles || defaults.consumptionBundles(appConfig);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
45
|
const _getProducts = (appConfig) => {
|
|
265
46
|
const productsObj = defaults.products(appConfig.packageName);
|
|
47
|
+
|
|
266
48
|
if (appConfig.env?.products) {
|
|
267
49
|
const customProducts = appConfig.env.products[0];
|
|
268
50
|
if (customProducts?.ordId?.toLowerCase().startsWith("sap")) {
|
|
@@ -273,40 +55,39 @@ const _getProducts = (appConfig) => {
|
|
|
273
55
|
_.assign(productsObj[0], customProducts);
|
|
274
56
|
}
|
|
275
57
|
}
|
|
276
|
-
|
|
58
|
+
|
|
277
59
|
return productsObj;
|
|
278
60
|
};
|
|
279
61
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
62
|
+
const _createDefaultORDDocument = (linkedCsn, appConfig, authConfig) => {
|
|
63
|
+
const products = _getProducts(appConfig);
|
|
64
|
+
const packages = defaults.packages(appConfig, products);
|
|
65
|
+
const packageIds = packages?.map((pkg) => pkg.ordId) || [];
|
|
66
|
+
const entityTypes = _getEntityTypes(appConfig, packageIds);
|
|
67
|
+
const integrationDependencies = getIntegrationDependencies(linkedCsn, appConfig, packageIds);
|
|
68
|
+
const apiResources = _getAPIResources(linkedCsn, appConfig, packageIds, authConfig.accessStrategies);
|
|
69
|
+
const eventResources = _getEventResources(linkedCsn, appConfig, packageIds, authConfig.accessStrategies);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
// Unconditionally added top-level properties
|
|
283
73
|
$schema: "https://open-resource-discovery.github.io/specification/spec-v1/interfaces/Document.schema.json",
|
|
284
|
-
openResourceDiscovery: _getOpenResourceDiscovery(appConfig),
|
|
285
74
|
policyLevels: appConfig.policyLevels,
|
|
286
|
-
|
|
287
|
-
|
|
75
|
+
packages: packages,
|
|
76
|
+
description: appConfig.env?.description || defaults.description,
|
|
77
|
+
openResourceDiscovery: appConfig.env?.openResourceDiscovery || defaults.openResourceDiscovery,
|
|
78
|
+
consumptionBundles: appConfig.env?.consumptionBundles || defaults.consumptionBundles(appConfig),
|
|
79
|
+
|
|
80
|
+
// Conditionally added top-level properties
|
|
81
|
+
...(!entityTypes.length ? {} : { entityTypes: entityTypes }),
|
|
82
|
+
...(!apiResources.length ? {} : { apiResources: apiResources }),
|
|
83
|
+
...(!eventResources.length ? {} : { eventResources: eventResources }),
|
|
84
|
+
...(appConfig.existingProductORDId ? {} : { products: [products[0]] }),
|
|
85
|
+
...(!appConfig.serviceNames.length ? {} : { groups: _getGroups(linkedCsn, appConfig) }),
|
|
86
|
+
...(!integrationDependencies.length ? {} : { integrationDependencies: integrationDependencies }),
|
|
288
87
|
};
|
|
88
|
+
};
|
|
289
89
|
|
|
290
|
-
|
|
291
|
-
ordDocument.groups = _getGroups(linkedCsn, appConfig);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (appConfig.env?.existingProductORDId) {
|
|
295
|
-
appConfig.existingProductORDId = appConfig.env.existingProductORDId;
|
|
296
|
-
} else {
|
|
297
|
-
ordDocument.products = [_getProducts(appConfig)[0]];
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
ordDocument.packages = _getPackages(appConfig);
|
|
301
|
-
|
|
302
|
-
return ordDocument;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function extractPackageIds(ordDocument) {
|
|
306
|
-
return ordDocument.packages?.map((pkg) => pkg.ordId) || [];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function _filterUnusedPackages(ordDocument) {
|
|
90
|
+
const _filterUnusedPackages = (ordDocument) => {
|
|
310
91
|
if (!ordDocument.packages?.length) return [];
|
|
311
92
|
|
|
312
93
|
const usedPackageIds = new Set();
|
|
@@ -322,52 +103,33 @@ function _filterUnusedPackages(ordDocument) {
|
|
|
322
103
|
});
|
|
323
104
|
|
|
324
105
|
return ordDocument.packages.filter((pkg) => usedPackageIds.has(pkg.ordId));
|
|
325
|
-
}
|
|
106
|
+
};
|
|
326
107
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
108
|
+
const _getEntityTypes = (appConfig, packageIds) => {
|
|
109
|
+
return appConfig.entityTypeTargets.flatMap((entity) => createEntityTypeTemplate(appConfig, packageIds, entity));
|
|
110
|
+
};
|
|
330
111
|
|
|
331
|
-
|
|
112
|
+
const _createAuthConfig = () => {
|
|
332
113
|
const authConfig = createAuthConfig();
|
|
114
|
+
|
|
115
|
+
// Create auth config and fail-closed on configuration errors
|
|
333
116
|
if (authConfig.error) {
|
|
334
117
|
throw new Error(`Authentication configuration error: ${authConfig.error}`);
|
|
335
118
|
}
|
|
336
|
-
const accessStrategies = authConfig.accessStrategies;
|
|
337
|
-
|
|
338
|
-
let ordDocument = createDefaultORDDocument(linkedCsn, appConfig);
|
|
339
|
-
const packageIds = extractPackageIds(ordDocument);
|
|
340
|
-
const entityTypes = _getEntityTypes(appConfig, packageIds);
|
|
341
119
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (appConfig.apiResourceNames.length) {
|
|
347
|
-
const apiResources = _getAPIResources(linkedCsn, appConfig, packageIds, accessStrategies);
|
|
348
|
-
if (apiResources.length) {
|
|
349
|
-
ordDocument.apiResources = apiResources;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (appConfig.eventServiceNames.length) {
|
|
353
|
-
const eventResources = _getEventResources(linkedCsn, appConfig, packageIds, accessStrategies);
|
|
354
|
-
if (eventResources.length) {
|
|
355
|
-
ordDocument.eventResources = eventResources;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const integrationDependencies = getIntegrationDependencies(linkedCsn, appConfig, packageIds);
|
|
360
|
-
if (integrationDependencies.length) {
|
|
361
|
-
ordDocument.integrationDependencies = integrationDependencies;
|
|
362
|
-
}
|
|
120
|
+
return authConfig;
|
|
121
|
+
};
|
|
363
122
|
|
|
364
|
-
|
|
123
|
+
module.exports = (csn, extensions = []) => {
|
|
124
|
+
const authConfig = _createAuthConfig();
|
|
125
|
+
const linkedCsn = _propagateORDVisibility(cds.linked(csn));
|
|
126
|
+
const appConfig = new Configuration(linkedCsn);
|
|
127
|
+
const ordDocument = [...(extensions || []), getCustomORDContent(appConfig)]
|
|
365
128
|
.filter((extension) => !!extension)
|
|
366
|
-
.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
ordDocument.packages = _filterUnusedPackages(ordDocument);
|
|
129
|
+
.reduce(
|
|
130
|
+
(document, extension) => compareAndHandleCustomORDContentWithExistingContent(document, extension),
|
|
131
|
+
_createDefaultORDDocument(linkedCsn, appConfig, authConfig),
|
|
132
|
+
);
|
|
371
133
|
|
|
372
|
-
return ordDocument;
|
|
134
|
+
return Object.assign(ordDocument, { packages: _filterUnusedPackages(ordDocument) });
|
|
373
135
|
};
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
const cds = require("@sap/cds/lib");
|
|
2
2
|
const { ord, getMetadata } = require("@cap-js/ord/lib");
|
|
3
3
|
|
|
4
|
+
const defaults = require("../defaults");
|
|
5
|
+
const { DOCUMENT_PERSPECTIVES } = require("../constants");
|
|
6
|
+
|
|
4
7
|
module.exports = class MtxOrdProviderService extends cds.ApplicationService {
|
|
5
8
|
init() {
|
|
6
9
|
this.on("getOrdDocument", async (req) => {
|
|
7
10
|
req._?.res?.set("Content-Type", "application/json");
|
|
8
11
|
|
|
9
|
-
return
|
|
10
|
-
|
|
11
|
-
await
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
return defaults.adjustForPerspective(
|
|
13
|
+
ord(
|
|
14
|
+
await (
|
|
15
|
+
await cds.connect.to("cds.xt.ModelProviderService")
|
|
16
|
+
).getCsn({
|
|
17
|
+
tenant: req.data.tenant,
|
|
18
|
+
toggles: req.data.toggles,
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
DOCUMENT_PERSPECTIVES.SystemInstance,
|
|
17
22
|
);
|
|
18
23
|
});
|
|
19
24
|
|
|
@@ -23,7 +28,6 @@ module.exports = class MtxOrdProviderService extends cds.ApplicationService {
|
|
|
23
28
|
await (
|
|
24
29
|
await cds.connect.to("cds.xt.ModelProviderService")
|
|
25
30
|
).getCsn({
|
|
26
|
-
for: req.data.for,
|
|
27
31
|
tenant: req.data.tenant,
|
|
28
32
|
toggles: req.data.toggles,
|
|
29
33
|
}),
|
|
@@ -3,9 +3,61 @@ const cds = require("@sap/cds");
|
|
|
3
3
|
const ord = require("../ord.js");
|
|
4
4
|
const Logger = require("../logger.js");
|
|
5
5
|
const defaults = require("../defaults.js");
|
|
6
|
+
const { LOCAL_TENANT_ID_HEADER_KEY, DOCUMENT_PERSPECTIVES } = require("../constants");
|
|
6
7
|
const compileMetadata = require("../meta-data.js");
|
|
7
8
|
const { createAuthConfig, createAuthMiddleware } = require("../auth/authentication.js");
|
|
8
9
|
|
|
10
|
+
const validationMiddleware = (req, res, next) => {
|
|
11
|
+
const toggles = cds.env.requires.toggles;
|
|
12
|
+
const perspective = req.query.perspective;
|
|
13
|
+
const extensibility = cds.env.requires.extensibility;
|
|
14
|
+
const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
|
|
15
|
+
|
|
16
|
+
if (perspective && ![DOCUMENT_PERSPECTIVES.SystemVersion, DOCUMENT_PERSPECTIVES.SystemInstance].includes(perspective)) {
|
|
17
|
+
return res.status(400).send(`Required query parameter 'perspective' is invalid`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (perspective === DOCUMENT_PERSPECTIVES.SystemInstance && !(toggles || extensibility)) {
|
|
21
|
+
return res.status(400).send(`Unsupported query parameter 'perspective=${perspective}'`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!tenant && perspective === DOCUMENT_PERSPECTIVES.SystemInstance) {
|
|
25
|
+
return res.status(400).send(`Missing required tenant context`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return next();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const metadataResponseHandler = async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const perspective = req.query.perspective;
|
|
34
|
+
const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
|
|
35
|
+
const model = await resolveCdsModel(perspective, tenant);
|
|
36
|
+
const { contentType, response } = await compileMetadata(req.path, model);
|
|
37
|
+
|
|
38
|
+
return res.status(200).contentType(contentType).send(response);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
Logger.error(error, "Error while processing the resource definition document");
|
|
41
|
+
return res.status(500).send(error.message);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const resolveCdsModel = async (perspective, tenant) => {
|
|
46
|
+
if (!tenant || perspective !== DOCUMENT_PERSPECTIVES.SystemInstance) {
|
|
47
|
+
Logger.info("Retrieving static CDS model...");
|
|
48
|
+
return cds.model;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Logger.info(`Retrieving dynamic CDS model for tenant ${tenant}...`);
|
|
52
|
+
|
|
53
|
+
return await (
|
|
54
|
+
await cds.connect.to("cds.xt.ModelProviderService")
|
|
55
|
+
).getCsn({
|
|
56
|
+
tenant: tenant,
|
|
57
|
+
toggles: OpenResourceDiscoveryService.resolveFeatureToggles(tenant),
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
9
61
|
class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
10
62
|
async init() {
|
|
11
63
|
this.extensions = {};
|
|
@@ -26,38 +78,38 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
|
26
78
|
// Create authentication middleware
|
|
27
79
|
const authMiddleware = createAuthMiddleware(authConfig);
|
|
28
80
|
|
|
29
|
-
|
|
30
|
-
|
|
81
|
+
// Default: /.well-known/open-resource-discovery
|
|
82
|
+
cds.app.get(`${this.path}`, (req, res) => {
|
|
83
|
+
const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
|
|
84
|
+
|
|
85
|
+
return res.status(200).send(defaults.baseTemplate(authConfig, tenant));
|
|
31
86
|
});
|
|
32
87
|
|
|
33
|
-
cds.app.get(`/ord/v1/documents/ord-document`, authMiddleware, async (
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
88
|
+
cds.app.get(`/ord/v1/documents/ord-document`, [authMiddleware, validationMiddleware], async (req, res) => {
|
|
89
|
+
const perspective = req.query.perspective || DOCUMENT_PERSPECTIVES.SystemVersion;
|
|
90
|
+
const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
|
|
91
|
+
const model = await resolveCdsModel(perspective, tenant);
|
|
92
|
+
const extensions = Array.from(Object.values(this.extensions));
|
|
93
|
+
|
|
94
|
+
return res.status(200).send(defaults.adjustForPerspective(ord(model, extensions), perspective));
|
|
37
95
|
});
|
|
38
96
|
|
|
39
|
-
cds.app.get(`/ord/v1/documents/:id`, authMiddleware, async (_, res) => {
|
|
97
|
+
cds.app.get(`/ord/v1/documents/:id`, [authMiddleware, validationMiddleware], async (_, res) => {
|
|
40
98
|
return res.status(404).send("404 Not Found");
|
|
41
99
|
});
|
|
42
100
|
|
|
43
|
-
// Handler for metadata requests (oas3, edmx, csn, etc.)
|
|
44
|
-
const metadataHandler = async (req, res) => {
|
|
45
|
-
try {
|
|
46
|
-
const { contentType, response } = await compileMetadata(req.url);
|
|
47
|
-
return res.status(200).contentType(contentType).send(response);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
Logger.error(error, "Error while processing the resource definition document");
|
|
50
|
-
return res.status(500).send(error.message);
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
101
|
// Use separate routes instead of optional parameters for path-to-regexp v8 compatibility
|
|
55
|
-
cds.app.get(`/ord/v1/:ordId/:service`, authMiddleware,
|
|
56
|
-
cds.app.get(`/ord/v1/:ordId`, authMiddleware,
|
|
57
|
-
cds.app.get(`/ord/v1`, authMiddleware,
|
|
102
|
+
cds.app.get(`/ord/v1/:ordId/:service`, [authMiddleware, validationMiddleware], metadataResponseHandler);
|
|
103
|
+
cds.app.get(`/ord/v1/:ordId`, [authMiddleware, validationMiddleware], metadataResponseHandler);
|
|
104
|
+
cds.app.get(`/ord/v1`, [authMiddleware, validationMiddleware], metadataResponseHandler);
|
|
58
105
|
|
|
59
106
|
return super.init();
|
|
60
107
|
}
|
|
108
|
+
|
|
109
|
+
// eslint-disable-next-line no-unused-vars
|
|
110
|
+
static resolveFeatureToggles(tenant) {
|
|
111
|
+
return ["*"];
|
|
112
|
+
}
|
|
61
113
|
}
|
|
62
114
|
|
|
63
115
|
module.exports = { OpenResourceDiscoveryService };
|
package/lib/templates.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { hasSAPPolicyLevel } = require("./utils");
|
|
2
1
|
const defaults = require("./defaults");
|
|
3
2
|
const _ = require("lodash");
|
|
4
3
|
const {
|
|
@@ -76,14 +75,22 @@ const createEntityTypeMappingsItemTemplate = (entity) => {
|
|
|
76
75
|
};
|
|
77
76
|
|
|
78
77
|
function _getGroupID(serviceDefinition, groupTypeId = defaults.groupTypeId, appConfig) {
|
|
79
|
-
return `${groupTypeId}:${appConfig.ordNamespace}:${
|
|
78
|
+
return `${groupTypeId}:${appConfig.ordNamespace}:${_getCleanServiceName(serviceDefinition, appConfig)}`;
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
function
|
|
83
|
-
if (!name.startsWith(
|
|
84
|
-
|
|
81
|
+
function _startsWithNamespace(name, namespace) {
|
|
82
|
+
if (!name.startsWith(namespace)) return false;
|
|
83
|
+
const rest = name.substring(namespace.length);
|
|
84
|
+
return rest === "" || rest.startsWith(".");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _getCleanServiceName({ name }, appConfig) {
|
|
88
|
+
let sortedName = name;
|
|
89
|
+
if (appConfig.internalNamespace && _startsWithNamespace(name, appConfig.internalNamespace)) {
|
|
90
|
+
sortedName = name.substring(appConfig.internalNamespace.length);
|
|
91
|
+
} else if (_startsWithNamespace(name, appConfig.ordNamespace)) {
|
|
92
|
+
sortedName = name.substring(appConfig.ordNamespace.length);
|
|
85
93
|
}
|
|
86
|
-
let sortedName = name.substring(appConfig.ordNamespace.length);
|
|
87
94
|
if (sortedName.startsWith(".")) {
|
|
88
95
|
sortedName = sortedName.substring(1);
|
|
89
96
|
}
|
|
@@ -157,7 +164,7 @@ function _getResourceDefinition(resourceType, mediaType, ordId, serviceName, fil
|
|
|
157
164
|
*
|
|
158
165
|
* @param {string} serviceName The name of the service.
|
|
159
166
|
* @param {object} serviceDefinition The definition of the service
|
|
160
|
-
* @param {
|
|
167
|
+
* @param {object} appConfig - The application configuration.
|
|
161
168
|
* @returns {Object} A group object.
|
|
162
169
|
*/
|
|
163
170
|
const createGroupsTemplateForService = (serviceName, serviceDefinition, appConfig) => {
|
|
@@ -197,7 +204,7 @@ const createEntityTypeTemplate = (appConfig, packageIds, entity) => {
|
|
|
197
204
|
// ODM mappings are not created as entity types, they are only used in entityTypeMappings
|
|
198
205
|
return [];
|
|
199
206
|
}
|
|
200
|
-
if (
|
|
207
|
+
if (appConfig.hasSAPPolicyLevel) {
|
|
201
208
|
// If SAP policy level is present, don't create entity type, they must be in the central repository
|
|
202
209
|
return [];
|
|
203
210
|
}
|
|
@@ -305,19 +312,19 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
305
312
|
extracted = _extractVersionFromServiceName(serviceDefinition.name);
|
|
306
313
|
if (extracted) {
|
|
307
314
|
// Create a temporary service definition with the clean name for namespace processing
|
|
308
|
-
const
|
|
309
|
-
cleanServiceName =
|
|
315
|
+
const versionExtractedServiceDefinition = { ...serviceDefinition, name: extracted.cleanName };
|
|
316
|
+
cleanServiceName = _getCleanServiceName(versionExtractedServiceDefinition, appConfig);
|
|
310
317
|
version = extracted.version;
|
|
311
318
|
semanticVersion = extracted.semanticVersion;
|
|
312
319
|
} else {
|
|
313
320
|
// Invalid pattern - use current behavior
|
|
314
|
-
cleanServiceName =
|
|
321
|
+
cleanServiceName = _getCleanServiceName(serviceDefinition, appConfig);
|
|
315
322
|
version = "v1";
|
|
316
323
|
semanticVersion = "1.0.0";
|
|
317
324
|
}
|
|
318
325
|
} else {
|
|
319
326
|
// Non-data product - use current behavior
|
|
320
|
-
cleanServiceName =
|
|
327
|
+
cleanServiceName = _getCleanServiceName(serviceDefinition, appConfig);
|
|
321
328
|
version = "v1";
|
|
322
329
|
semanticVersion = "1.0.0";
|
|
323
330
|
}
|
|
@@ -349,7 +356,14 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
349
356
|
];
|
|
350
357
|
} else if (apiProtocol === ORD_API_PROTOCOL.MCP) {
|
|
351
358
|
resourceDefinitions = [
|
|
352
|
-
_getResourceDefinition(
|
|
359
|
+
_getResourceDefinition(
|
|
360
|
+
MCP_RESOURCE_DEFINITION_TYPE,
|
|
361
|
+
"json",
|
|
362
|
+
ordId,
|
|
363
|
+
serviceName,
|
|
364
|
+
"mcp.json",
|
|
365
|
+
accessStrategies,
|
|
366
|
+
),
|
|
353
367
|
];
|
|
354
368
|
} else if (apiProtocol === ORD_API_PROTOCOL.GRAPHQL) {
|
|
355
369
|
// GraphQL only has GraphQL SDL
|
|
@@ -363,8 +377,13 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
363
377
|
}),
|
|
364
378
|
},
|
|
365
379
|
];
|
|
380
|
+
} else if (apiProtocol === ORD_API_PROTOCOL.ODATA_V2) {
|
|
381
|
+
// openapi-v3 is not supported for OData V2, only EDMX
|
|
382
|
+
resourceDefinitions = [
|
|
383
|
+
_getResourceDefinition("edmx", "xml", ordId, serviceName, "edmx", accessStrategies),
|
|
384
|
+
];
|
|
366
385
|
} else {
|
|
367
|
-
//
|
|
386
|
+
// odata-v4 and others have both OpenAPI and EDMX
|
|
368
387
|
resourceDefinitions = [
|
|
369
388
|
_getResourceDefinition("openapi-v3", "json", ordId, serviceName, "oas3.json", accessStrategies),
|
|
370
389
|
_getResourceDefinition("edmx", "xml", ordId, serviceName, "edmx", accessStrategies),
|
|
@@ -372,7 +391,6 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
372
391
|
}
|
|
373
392
|
}
|
|
374
393
|
|
|
375
|
-
const entityTypeMappings = _getEntityTypeMappings(serviceDefinition);
|
|
376
394
|
const exposedEntityTypes = _getExposedEntityTypes(serviceDefinition);
|
|
377
395
|
|
|
378
396
|
let obj = {
|
|
@@ -392,7 +410,6 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
392
410
|
extensible: {
|
|
393
411
|
supported: "no",
|
|
394
412
|
},
|
|
395
|
-
...(entityTypeMappings ? { entityTypeMappings } : {}),
|
|
396
413
|
...(exposedEntityTypes ? { exposedEntityTypes } : []),
|
|
397
414
|
...ordExtensions,
|
|
398
415
|
};
|
|
@@ -431,8 +448,7 @@ const createEventResourceTemplate = (serviceName, serviceDefinition, appConfig,
|
|
|
431
448
|
const ordExtensions = readORDExtensions(serviceDefinition);
|
|
432
449
|
const visibility = _handleVisibility(ordExtensions, serviceDefinition, appConfig.env?.defaultVisibility);
|
|
433
450
|
const packageId = _getPackageID(appConfig.ordNamespace, packageIds, ORD_RESOURCE_TYPE.event, visibility);
|
|
434
|
-
const ordId = `${appConfig.ordNamespace}:eventResource:${
|
|
435
|
-
const entityTypeMappings = _getEntityTypeMappings(serviceDefinition);
|
|
451
|
+
const ordId = `${appConfig.ordNamespace}:eventResource:${_getCleanServiceName(serviceDefinition, appConfig)}:v1`;
|
|
436
452
|
const exposedEntityTypes = _getExposedEntityTypes(serviceDefinition);
|
|
437
453
|
|
|
438
454
|
let obj = {
|
|
@@ -456,7 +472,6 @@ const createEventResourceTemplate = (serviceName, serviceDefinition, appConfig,
|
|
|
456
472
|
_getResourceDefinition("asyncapi-v2", "json", ordId, serviceName, "asyncapi2.json", accessStrategies),
|
|
457
473
|
],
|
|
458
474
|
extensible: { supported: "no" },
|
|
459
|
-
...(entityTypeMappings ? { entityTypeMappings } : {}),
|
|
460
475
|
...(exposedEntityTypes ? { exposedEntityTypes } : []),
|
|
461
476
|
...ordExtensions,
|
|
462
477
|
};
|
|
@@ -473,28 +488,6 @@ function isPrimaryDataProductService(serviceDefinition) {
|
|
|
473
488
|
);
|
|
474
489
|
}
|
|
475
490
|
|
|
476
|
-
function _getEntityTypeMappings(definitionObj) {
|
|
477
|
-
if (!definitionObj.entities) {
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const entities = Object.values(definitionObj.entities).flatMap((entity) => {
|
|
481
|
-
const entityData = _flattenEntityGraph(entity)
|
|
482
|
-
.flatMap(createEntityTypeMappingsItemTemplate) // now returns arrays
|
|
483
|
-
.filter(Boolean);
|
|
484
|
-
return _.uniqBy(entityData, CONTENT_MERGE_KEY);
|
|
485
|
-
});
|
|
486
|
-
const entityTypeTargets = _.uniqBy(entities, CONTENT_MERGE_KEY)
|
|
487
|
-
.filter((entity) => entity !== undefined)
|
|
488
|
-
.map(({ ordId }) => ({
|
|
489
|
-
ordId,
|
|
490
|
-
}));
|
|
491
|
-
if (entityTypeTargets.length > 0) {
|
|
492
|
-
return [{ entityTypeTargets }];
|
|
493
|
-
} else {
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
491
|
function _getExposedEntityTypes(definitionObj) {
|
|
499
492
|
if (!definitionObj.entities) {
|
|
500
493
|
return;
|
|
@@ -508,10 +501,9 @@ function _getExposedEntityTypes(definitionObj) {
|
|
|
508
501
|
.map(({ ordId }) => ({
|
|
509
502
|
ordId,
|
|
510
503
|
}));
|
|
504
|
+
|
|
511
505
|
if (exposedEntityTypes.length > 0) {
|
|
512
506
|
return exposedEntityTypes;
|
|
513
|
-
} else {
|
|
514
|
-
return;
|
|
515
507
|
}
|
|
516
508
|
}
|
|
517
509
|
|
|
@@ -614,7 +606,6 @@ module.exports = {
|
|
|
614
606
|
createEventResourceTemplate,
|
|
615
607
|
readORDExtensions,
|
|
616
608
|
_getPackageID,
|
|
617
|
-
_getEntityTypeMappings,
|
|
618
609
|
_getExposedEntityTypes,
|
|
619
610
|
_propagateORDVisibility,
|
|
620
611
|
_handleVisibility,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/ord",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "CAP Plugin for generating ORD document.",
|
|
5
5
|
"repository": "cap-js/ord",
|
|
6
6
|
"author": "SAP SE (https://www.sap.com)",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"test:integration:mtls": "jest __tests__/integration/mtls-auth.test.js --testPathIgnorePatterns=/node_modules/ --forceExit",
|
|
26
26
|
"test:integration:build": "jest __tests__/integration/cds-build.test.js --testPathIgnorePatterns=/node_modules/ --forceExit",
|
|
27
27
|
"update-snapshot": "jest --ci --updateSnapshot",
|
|
28
|
-
"cds:version": "cds v -i"
|
|
28
|
+
"cds:version": "cds v -i",
|
|
29
|
+
"test:all": "npm run test && npm run test:integration:basic && npm run test:integration:mtls && npm run test:integration:build"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@cap-js/graphql": "^0.14.0",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"node": ">=20 <25"
|
|
53
54
|
},
|
|
54
55
|
"overrides": {
|
|
55
|
-
"@sap/cds-compiler": "6.9.
|
|
56
|
+
"@sap/cds-compiler": "6.9.2"
|
|
56
57
|
},
|
|
57
58
|
"cds": {
|
|
58
59
|
"requires": {
|
package/lib/utils.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checks if at least one policy level in the array is SAP.
|
|
3
|
-
*
|
|
4
|
-
* @param {string[]} policyLevels - Array of policy levels.
|
|
5
|
-
*/
|
|
6
|
-
function hasSAPPolicyLevel(policyLevels) {
|
|
7
|
-
return policyLevels.some((policyLevel) => policyLevel.split(":")[0].toLowerCase() === "sap");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
module.exports = {
|
|
11
|
-
hasSAPPolicyLevel,
|
|
12
|
-
};
|