@cap-js/ord 1.8.0 → 1.9.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/README.md +10 -5
- package/lib/auth/authentication.js +30 -33
- package/lib/auth/cf-mtls.js +5 -5
- package/lib/build.js +1 -3
- package/lib/common/placeholders.js +6 -0
- package/lib/common/slice.js +72 -0
- package/lib/common/utils.js +190 -0
- package/lib/configuration.js +33 -88
- package/lib/constants.js +2 -32
- package/lib/defaults.js +27 -128
- package/lib/extend-ord-with-custom.js +82 -51
- package/lib/interop-csn.js +23 -11
- package/lib/logger.js +1 -3
- package/lib/meta-data.js +14 -26
- package/lib/ord.js +59 -122
- package/lib/protocol-resolver.js +26 -83
- package/lib/services/ord-service.js +30 -6
- package/lib/templates/api-resource.js +283 -0
- package/lib/templates/entity-type.js +121 -0
- package/lib/templates/event-resource.js +161 -0
- package/lib/templates/group.js +50 -0
- package/lib/templates/integration-dependency.js +98 -0
- package/lib/templates/package.js +91 -0
- package/lib/templates/product.js +57 -0
- package/package.json +6 -4
- package/data/well-known.json +0 -14
- package/lib/access-strategies.js +0 -172
- package/lib/date.js +0 -17
- package/lib/integration-dependency.js +0 -126
- package/lib/templates.js +0 -625
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
const _ = require("lodash");
|
|
2
|
+
|
|
3
|
+
const Logger = require("../logger");
|
|
4
|
+
const defaults = require("../defaults");
|
|
5
|
+
const { createPackages } = require("./package");
|
|
6
|
+
const placeholders = require("../common/placeholders");
|
|
7
|
+
const { resolveApiResourceProtocol } = require("../protocol-resolver");
|
|
8
|
+
const { resolveVisibility, resolveServiceName, flattenEntityGraph } = require("../common/utils");
|
|
9
|
+
const {
|
|
10
|
+
isValidService,
|
|
11
|
+
readORDExtensions,
|
|
12
|
+
isEventsOnlyService,
|
|
13
|
+
isPrimaryDataProductService,
|
|
14
|
+
} = require("../common/utils");
|
|
15
|
+
const {
|
|
16
|
+
DESCRIPTION_PREFIX,
|
|
17
|
+
ORD_RESOURCE_TYPE,
|
|
18
|
+
RESOURCE_VISIBILITY,
|
|
19
|
+
SHORT_DESCRIPTION_PREFIX,
|
|
20
|
+
ORD_API_PROTOCOL,
|
|
21
|
+
MCP_RESOURCE_DEFINITION_TYPE,
|
|
22
|
+
ORD_EXTENSIONS_PREFIX,
|
|
23
|
+
ORD_ODM_ENTITY_NAME_ANNOTATION,
|
|
24
|
+
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
25
|
+
} = require("../constants");
|
|
26
|
+
|
|
27
|
+
const RESOURCE_DEFINITION_PROVIDERS = Object.freeze({
|
|
28
|
+
[ORD_API_PROTOCOL.SAP_INA]: () => [],
|
|
29
|
+
[ORD_API_PROTOCOL.MCP]: (service, ordId, accessStrategies) => {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
mediaType: `application/json`,
|
|
33
|
+
type: MCP_RESOURCE_DEFINITION_TYPE,
|
|
34
|
+
accessStrategies: accessStrategies,
|
|
35
|
+
url: `/ord/v1/${ordId}/${service.name}.mcp.json`,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
},
|
|
39
|
+
[ORD_API_PROTOCOL.REST]: (service, ordId, accessStrategies) => {
|
|
40
|
+
// REST only has OpenAPI, no EDMX
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
type: "openapi-v3",
|
|
44
|
+
mediaType: `application/json`,
|
|
45
|
+
accessStrategies: accessStrategies,
|
|
46
|
+
url: `/ord/v1/${ordId}/${service.name}.oas3.json`,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
},
|
|
50
|
+
[ORD_API_PROTOCOL.GRAPHQL]: (service, ordId, accessStrategies) => {
|
|
51
|
+
// GraphQL only has GraphQL SDL
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
type: "graphql-sdl",
|
|
55
|
+
mediaType: "text/plain",
|
|
56
|
+
accessStrategies: accessStrategies,
|
|
57
|
+
url: `/ord/v1/${ordId}/${service.name}.graphql`,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
},
|
|
61
|
+
[ORD_API_PROTOCOL.ODATA_V2]: (service, ordId, accessStrategies) => {
|
|
62
|
+
// openapi-v3 is not supported for OData V2, only EDMX
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
type: "edmx",
|
|
66
|
+
mediaType: "application/xml",
|
|
67
|
+
accessStrategies: accessStrategies,
|
|
68
|
+
url: `/ord/v1/${ordId}/${service.name}.edmx`,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
},
|
|
72
|
+
[ORD_API_PROTOCOL.ODATA_V4]: (service, ordId, accessStrategies) => {
|
|
73
|
+
// OData V4 supports both openapi-v3 and EDMX
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
type: "openapi-v3",
|
|
77
|
+
mediaType: "application/json",
|
|
78
|
+
accessStrategies: accessStrategies,
|
|
79
|
+
url: `/ord/v1/${ordId}/${service.name}.oas3.json`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "edmx",
|
|
83
|
+
mediaType: "application/xml",
|
|
84
|
+
accessStrategies: accessStrategies,
|
|
85
|
+
url: `/ord/v1/${ordId}/${service.name}.edmx`,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
},
|
|
89
|
+
[ORD_API_PROTOCOL.SAP_DATA_SUBSCRIPTION]: (service, ordId, accessStrategies) => {
|
|
90
|
+
// Data product services use CSN
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
mediaType: `application/json`,
|
|
94
|
+
accessStrategies: accessStrategies,
|
|
95
|
+
type: "sap-csn-interop-effective-v1",
|
|
96
|
+
url: `/ord/v1/${ordId}/${service.name}.csn.json`,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function _getGroupID(appConfig, srvDefinition) {
|
|
103
|
+
return `${defaults.groupTypeId}:${appConfig.ordNamespace}:${resolveServiceName(appConfig, srvDefinition)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _getExposedEntityTypes(service) {
|
|
107
|
+
const ordIds = new Set(
|
|
108
|
+
Object.values(service.entities ?? {})
|
|
109
|
+
.flatMap((entity) => flattenEntityGraph(entity))
|
|
110
|
+
.flatMap((entity) => {
|
|
111
|
+
const [namespace, name, version = "v1"] = entity[ENTITY_RELATIONSHIP_ANNOTATION]?.split(":") || [];
|
|
112
|
+
const major = (entity[`${ORD_EXTENSIONS_PREFIX}version`] ?? version.substring(1)).split(".")[0];
|
|
113
|
+
|
|
114
|
+
return [
|
|
115
|
+
...(!entity[ORD_ODM_ENTITY_NAME_ANNOTATION]
|
|
116
|
+
? []
|
|
117
|
+
: [`sap.odm:entityType:${entity[ORD_ODM_ENTITY_NAME_ANNOTATION]}:v1`]),
|
|
118
|
+
...(!entity[ENTITY_RELATIONSHIP_ANNOTATION]
|
|
119
|
+
? []
|
|
120
|
+
: [entity["@ORD.Extensions.ordId"] ?? `${namespace}:entityType:${name}:v${major}`]),
|
|
121
|
+
];
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return [...ordIds].map((ordId) => ({ ordId: ordId }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extracts version suffix from service name for data product services.
|
|
130
|
+
* Only accepts pattern: .v<number> (e.g., .v0, .v1, .v2, .v10)
|
|
131
|
+
* Rejects patterns like: .v1.1, .v1.0, .version1, .beta
|
|
132
|
+
*
|
|
133
|
+
* @param {string} serviceName The full service name
|
|
134
|
+
* @returns {Object|null} Object with cleanName, version, and semanticVersion, or null if invalid pattern
|
|
135
|
+
*/
|
|
136
|
+
function _extractVersionFromServiceName(serviceName) {
|
|
137
|
+
// Only match pattern: .v<number> (where number is 1 or more digits)
|
|
138
|
+
const versionPattern = /\.v(\d+)$/;
|
|
139
|
+
const match = serviceName.match(versionPattern);
|
|
140
|
+
|
|
141
|
+
if (!match) {
|
|
142
|
+
return null; // No valid version suffix found
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const versionNumber = parseInt(match[1], 10);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
cleanName: serviceName.replace(versionPattern, ""),
|
|
149
|
+
semanticVersion: `${versionNumber}.0.0`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _getPackageID(namespace, packageIds, resourceType, visibility = RESOURCE_VISIBILITY.public) {
|
|
154
|
+
return (
|
|
155
|
+
packageIds.find((id) => {
|
|
156
|
+
if (visibility === RESOURCE_VISIBILITY.public) {
|
|
157
|
+
return id.includes(resourceType) && !id.includes("-internal") && !id.includes("-private");
|
|
158
|
+
} else {
|
|
159
|
+
return id.includes(`${resourceType}-${visibility}`);
|
|
160
|
+
}
|
|
161
|
+
}) || packageIds.find((id) => id.includes(namespace))
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* This is a template function to create API Resource object for API Resource Array.
|
|
167
|
+
* Properties of an API resource can be overwritten by the ORD extensions. Example: visibility.
|
|
168
|
+
* Ensures proper visibility compliance by checking associated EntityTypes.
|
|
169
|
+
* @param {object} srvDefinition The definition of the service
|
|
170
|
+
* @param {object} appConfig - The application configuration.
|
|
171
|
+
* @returns {Array} An array of objects for the API Resources.
|
|
172
|
+
*/
|
|
173
|
+
const createAPIResourceTemplate = (srvDefinition, appConfig) => {
|
|
174
|
+
const apiResources = [];
|
|
175
|
+
const namespace = appConfig.ordNamespace;
|
|
176
|
+
const accessStrategies = appConfig.accessStrategies;
|
|
177
|
+
const ordExtensions = readORDExtensions(srvDefinition);
|
|
178
|
+
const visibility = resolveVisibility(appConfig, srvDefinition);
|
|
179
|
+
const packageIds = createPackages(appConfig)?.map((pkg) => pkg.ordId) || [];
|
|
180
|
+
const packageId = _getPackageID(namespace, packageIds, ORD_RESOURCE_TYPE.api, visibility);
|
|
181
|
+
const protocolResults = resolveApiResourceProtocol(srvDefinition);
|
|
182
|
+
|
|
183
|
+
// If no protocols were generated, skip this service
|
|
184
|
+
if (protocolResults.length === 0) {
|
|
185
|
+
Logger.info(`No supported protocols for service '${srvDefinition.name}', skipping API resource generation.`);
|
|
186
|
+
return apiResources;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle version suffix extraction for primary data product services
|
|
190
|
+
let cleanServiceName,
|
|
191
|
+
version,
|
|
192
|
+
semanticVersion,
|
|
193
|
+
extracted = null;
|
|
194
|
+
if (isPrimaryDataProductService(srvDefinition)) {
|
|
195
|
+
extracted = _extractVersionFromServiceName(srvDefinition.name);
|
|
196
|
+
|
|
197
|
+
cleanServiceName = resolveServiceName(appConfig, { name: extracted?.cleanName || srvDefinition.name });
|
|
198
|
+
semanticVersion = ordExtensions.version || extracted?.semanticVersion || "1.0.0";
|
|
199
|
+
version = `v${semanticVersion.split(".")[0]}`;
|
|
200
|
+
} else {
|
|
201
|
+
// Non-data product - use current behavior
|
|
202
|
+
cleanServiceName = resolveServiceName(appConfig, srvDefinition);
|
|
203
|
+
semanticVersion = ordExtensions.version || "1.0.0";
|
|
204
|
+
version = `v${semanticVersion.split(".")[0]}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protocolResults.forEach((protocolResult, index) => {
|
|
208
|
+
const { apiProtocol, entryPoints, hasResourceDefinitions } = protocolResult;
|
|
209
|
+
const protocolExtensions = readORDExtensions(srvDefinition, `@protocol('${apiProtocol}').ORD.Extensions.`);
|
|
210
|
+
const ordId =
|
|
211
|
+
placeholders.replace(protocolExtensions.ordId, { type: "apiResource", namespace: namespace }) ??
|
|
212
|
+
placeholders.replace(ordExtensions.ordId, { type: "apiResource", namespace: namespace }) ??
|
|
213
|
+
`${namespace}:apiResource:${cleanServiceName}${index === 0 ? "" : `-${apiProtocol}`}:${version}`;
|
|
214
|
+
|
|
215
|
+
// Build resource definitions based on protocol
|
|
216
|
+
const resourceDefinitions = !hasResourceDefinitions
|
|
217
|
+
? []
|
|
218
|
+
: RESOURCE_DEFINITION_PROVIDERS[apiProtocol](srvDefinition, ordId, accessStrategies);
|
|
219
|
+
|
|
220
|
+
const exposedEntityTypes = _getExposedEntityTypes(srvDefinition);
|
|
221
|
+
|
|
222
|
+
let obj = {
|
|
223
|
+
ordId,
|
|
224
|
+
title:
|
|
225
|
+
srvDefinition["@title"] ??
|
|
226
|
+
srvDefinition["@Common.Label"] ??
|
|
227
|
+
srvDefinition["@EndUserText.label"] ??
|
|
228
|
+
srvDefinition.name,
|
|
229
|
+
shortDescription: SHORT_DESCRIPTION_PREFIX + srvDefinition.name,
|
|
230
|
+
description: srvDefinition["@Core.Description"] ?? DESCRIPTION_PREFIX + srvDefinition.name,
|
|
231
|
+
version: semanticVersion,
|
|
232
|
+
lastUpdate: appConfig.lastUpdate,
|
|
233
|
+
visibility,
|
|
234
|
+
partOfPackage: packageId,
|
|
235
|
+
partOfGroups: [_getGroupID(appConfig, srvDefinition)],
|
|
236
|
+
releaseStatus: "active",
|
|
237
|
+
apiProtocol,
|
|
238
|
+
resourceDefinitions,
|
|
239
|
+
entryPoints,
|
|
240
|
+
extensible: {
|
|
241
|
+
supported: "no",
|
|
242
|
+
},
|
|
243
|
+
...(exposedEntityTypes.length ? { exposedEntityTypes } : []),
|
|
244
|
+
..._.omit(ordExtensions, ["ordId"]),
|
|
245
|
+
..._.omit(protocolExtensions, ["ordId"]),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Special handling for data product services
|
|
249
|
+
if (isPrimaryDataProductService(srvDefinition)) {
|
|
250
|
+
obj.direction = "outbound";
|
|
251
|
+
if (extracted) {
|
|
252
|
+
// Overwrite partOfGroups
|
|
253
|
+
obj.partOfGroups = [`${defaults.groupTypeId}:${namespace}:${cleanServiceName}`];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
apiResources.push(obj);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return apiResources;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function createAPIResources(appConfig) {
|
|
264
|
+
return Object.values(appConfig.csn.definitions)
|
|
265
|
+
.filter((definition) => isValidService(definition) && !isEventsOnlyService(definition))
|
|
266
|
+
.flatMap((service) => createAPIResourceTemplate(service, appConfig))
|
|
267
|
+
.filter((ar) => ar.visibility !== RESOURCE_VISIBILITY.private)
|
|
268
|
+
.map((ar) => ({
|
|
269
|
+
...ar,
|
|
270
|
+
ordId: placeholders.replace(ar.ordId, { type: "apiResource", namespace: appConfig.ordNamespace }),
|
|
271
|
+
partOfPackage: placeholders.replace(ar.partOfPackage, {
|
|
272
|
+
type: "package",
|
|
273
|
+
namespace: appConfig.ordNamespace,
|
|
274
|
+
}),
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
createAPIResources,
|
|
280
|
+
createAPIResourceTemplate,
|
|
281
|
+
_getPackageID,
|
|
282
|
+
_getExposedEntityTypes,
|
|
283
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const _ = require("lodash");
|
|
2
|
+
|
|
3
|
+
const Logger = require("../logger");
|
|
4
|
+
const { createPackages } = require("./package");
|
|
5
|
+
const placeholders = require("../common/placeholders");
|
|
6
|
+
const { readORDExtensions, isExposedEntityType } = require("../common/utils");
|
|
7
|
+
const {
|
|
8
|
+
LEVEL,
|
|
9
|
+
SEM_VERSION_REGEX,
|
|
10
|
+
RESOURCE_VISIBILITY,
|
|
11
|
+
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
12
|
+
CONTENT_MERGE_KEY,
|
|
13
|
+
} = require("../constants");
|
|
14
|
+
|
|
15
|
+
const RESOLVERS = Object.freeze({
|
|
16
|
+
ordId: (entity, appConfig) => {
|
|
17
|
+
const [namespace, name, version = "v1"] = entity[ENTITY_RELATIONSHIP_ANNOTATION].split(":");
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
placeholders.replace(entity["@ORD.Extensions.ordId"], {
|
|
21
|
+
type: "entityType",
|
|
22
|
+
namespace: appConfig.ordNamespace,
|
|
23
|
+
}) ??
|
|
24
|
+
`${namespace}:entityType:${name}:v${(entity["@ORD.Extensions.version"] ?? version.substring(1)).split(".")[0]}`
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
title: (entity) => {
|
|
28
|
+
return (
|
|
29
|
+
entity["@ORD.Extensions.title"] ??
|
|
30
|
+
entity["@title"] ??
|
|
31
|
+
entity["@Common.Label"] ??
|
|
32
|
+
entity["@EndUserText.label"] ??
|
|
33
|
+
RESOLVERS.localId(entity)
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
level: (entity) => {
|
|
37
|
+
return (
|
|
38
|
+
entity["@ORD.Extensions.level"] ??
|
|
39
|
+
(entity["@ObjectModel.compositionRoot"] || entity["@ODM.root"] ? LEVEL.rootEntity : LEVEL.subEntity)
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
localId: (entity) => {
|
|
43
|
+
return entity["@ORD.Extensions.localId"] ?? entity[ENTITY_RELATIONSHIP_ANNOTATION].split(":")[1];
|
|
44
|
+
},
|
|
45
|
+
version: (entity, appConfig) => {
|
|
46
|
+
const ordId = RESOLVERS.ordId(entity, appConfig);
|
|
47
|
+
const version = entity["@ORD.Extensions.version"] ?? ordId.split(":").pop().replace("v", "") + ".0.0";
|
|
48
|
+
|
|
49
|
+
if (!SEM_VERSION_REGEX.test(version)) {
|
|
50
|
+
Logger.warn("Entity version", version, "is not a valid semantic version.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return version;
|
|
54
|
+
},
|
|
55
|
+
visibility: (entity, appConfig) => {
|
|
56
|
+
return entity["@ORD.Extensions.visibility"] ?? appConfig.env?.defaultVisibility ?? RESOURCE_VISIBILITY.public;
|
|
57
|
+
},
|
|
58
|
+
partOfPackage: (entity, appConfig) => {
|
|
59
|
+
const namespace = appConfig.ordNamespace;
|
|
60
|
+
const visibility = RESOLVERS.visibility(entity, appConfig);
|
|
61
|
+
const name = appConfig.appName?.replace(/[^a-zA-Z0-9]/g, "");
|
|
62
|
+
const packages = createPackages(appConfig).map((pkg) => pkg.ordId);
|
|
63
|
+
const suffix = visibility === RESOURCE_VISIBILITY.public ? "" : `-${visibility}`;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
placeholders.replace(entity["@ORD.Extensions.partOfPackage"], {
|
|
67
|
+
type: "package",
|
|
68
|
+
namespace: namespace,
|
|
69
|
+
}) ??
|
|
70
|
+
[`${namespace}:package:${name}-entityType${suffix}:v1`, `${namespace}:package:${name}:v1`].find(
|
|
71
|
+
(candidate) => packages.includes(candidate),
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This is a template function to create EntityType object for EntityTypes Array.
|
|
79
|
+
* Ensures correct visibility assignment based on referenced resources.
|
|
80
|
+
*
|
|
81
|
+
* @param { object } appConfig The configuration object.
|
|
82
|
+
* @param { object } entity The entity definition.
|
|
83
|
+
* @returns { object } An object for the EntityType (see: https://pages.github.tools.sap/CentralEngineering/open-resource-discovery-specification/spec-v1/interfaces/Document#entity-type).
|
|
84
|
+
*/
|
|
85
|
+
function createEntityTypeTemplate(appConfig, entity) {
|
|
86
|
+
return {
|
|
87
|
+
releaseStatus: "active",
|
|
88
|
+
extensible: { supported: "no" },
|
|
89
|
+
lastUpdate: appConfig.lastUpdate,
|
|
90
|
+
description: `Description for ${RESOLVERS.localId(entity)}`,
|
|
91
|
+
shortDescription: `Short description of ${RESOLVERS.localId(entity)}`,
|
|
92
|
+
|
|
93
|
+
title: RESOLVERS.title(entity),
|
|
94
|
+
level: RESOLVERS.level(entity),
|
|
95
|
+
localId: RESOLVERS.localId(entity),
|
|
96
|
+
ordId: RESOLVERS.ordId(entity, appConfig),
|
|
97
|
+
version: RESOLVERS.version(entity, appConfig),
|
|
98
|
+
visibility: RESOLVERS.visibility(entity, appConfig),
|
|
99
|
+
partOfPackage: RESOLVERS.partOfPackage(entity, appConfig),
|
|
100
|
+
|
|
101
|
+
..._.omit(readORDExtensions(entity), Object.keys(RESOLVERS)),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createEntityTypes(appConfig) {
|
|
106
|
+
return appConfig.hasSAPPolicyLevel
|
|
107
|
+
? [] // If SAP policy level is present, don't create entity type, they must be in the central repository
|
|
108
|
+
: _.uniqBy(
|
|
109
|
+
Object.values(appConfig.csn.definitions)
|
|
110
|
+
.filter((definition) => isExposedEntityType(definition))
|
|
111
|
+
.map((entity) => createEntityTypeTemplate(appConfig, entity))
|
|
112
|
+
.filter((entity) => entity.visibility !== RESOURCE_VISIBILITY.private),
|
|
113
|
+
CONTENT_MERGE_KEY,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
createEntityTypes,
|
|
119
|
+
createEntityTypeTemplate,
|
|
120
|
+
RESOLVERS,
|
|
121
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const _ = require("lodash");
|
|
2
|
+
|
|
3
|
+
const defaults = require("../defaults");
|
|
4
|
+
const { prune } = require("../common/utils");
|
|
5
|
+
const { createPackages } = require("./package");
|
|
6
|
+
const entityTypeTemplate = require("./entity-type");
|
|
7
|
+
const placeholders = require("../common/placeholders");
|
|
8
|
+
const {
|
|
9
|
+
RESOURCE_VISIBILITY,
|
|
10
|
+
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
11
|
+
ORD_ODM_ENTITY_NAME_ANNOTATION,
|
|
12
|
+
CDS_ELEMENT_KIND,
|
|
13
|
+
} = require("../constants");
|
|
14
|
+
const {
|
|
15
|
+
readORDExtensions,
|
|
16
|
+
resolveVisibility,
|
|
17
|
+
flattenEntityGraph,
|
|
18
|
+
resolveServiceName,
|
|
19
|
+
isValidService,
|
|
20
|
+
} = require("../common/utils");
|
|
21
|
+
|
|
22
|
+
const RESOLVERS = Object.freeze({
|
|
23
|
+
version: (service) => {
|
|
24
|
+
return service["@ORD.Extensions.version"] ?? "1.0.0";
|
|
25
|
+
},
|
|
26
|
+
description: (service) => {
|
|
27
|
+
return (
|
|
28
|
+
service["@ORD.Extensions.description"] ??
|
|
29
|
+
service["@description"] ??
|
|
30
|
+
service["@Core.Description"] ??
|
|
31
|
+
"CAP Event resource describing events / messages."
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
ordId: (service, appConfig) => {
|
|
35
|
+
const name = resolveServiceName(appConfig, service);
|
|
36
|
+
const version = `v${RESOLVERS.version(service).split(".")[0]}`;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
placeholders.replace(service["@ORD.Extensions.ordId"], {
|
|
40
|
+
type: "eventResource",
|
|
41
|
+
namespace: appConfig.ordNamespace,
|
|
42
|
+
}) ?? `${appConfig.ordNamespace}:eventResource:${name}:${version}`
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
title: (service, appConfig) => {
|
|
46
|
+
return (
|
|
47
|
+
service["@ORD.Extensions.title"] ??
|
|
48
|
+
service["@title"] ??
|
|
49
|
+
service["@Common.Label"] ??
|
|
50
|
+
service["@EndUserText.label"] ??
|
|
51
|
+
`ODM ${appConfig.appName.replace(/[^a-zA-Z0-9]/g, "")} Events`
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
exposedEntityTypes: (service, appConfig) => {
|
|
55
|
+
const ordIds = new Set(
|
|
56
|
+
Object.values(service.entities ?? {})
|
|
57
|
+
.flatMap((entity) => flattenEntityGraph(entity))
|
|
58
|
+
.flatMap((entity) => {
|
|
59
|
+
return [
|
|
60
|
+
...(!entity[ORD_ODM_ENTITY_NAME_ANNOTATION]
|
|
61
|
+
? []
|
|
62
|
+
: [`sap.odm:entityType:${entity[ORD_ODM_ENTITY_NAME_ANNOTATION]}:v1`]),
|
|
63
|
+
...(!entity[ENTITY_RELATIONSHIP_ANNOTATION]
|
|
64
|
+
? []
|
|
65
|
+
: [entityTypeTemplate.RESOLVERS.ordId(entity, appConfig)]),
|
|
66
|
+
];
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return service["@ORD.Extensions.exposedEntityTypes"] ?? [...ordIds].map((ordId) => ({ ordId: ordId }));
|
|
71
|
+
},
|
|
72
|
+
visibility: (service, appConfig) => {
|
|
73
|
+
return resolveVisibility(appConfig, service);
|
|
74
|
+
},
|
|
75
|
+
partOfGroups: (service, appConfig) => {
|
|
76
|
+
const namespace = appConfig.ordNamespace;
|
|
77
|
+
const name = resolveServiceName(appConfig, service);
|
|
78
|
+
|
|
79
|
+
return service["@ORD.Extensions.partOfGroups"] ?? [`${defaults.groupTypeId}:${namespace}:${name}`];
|
|
80
|
+
},
|
|
81
|
+
partOfPackage: (service, appConfig) => {
|
|
82
|
+
const namespace = appConfig.ordNamespace;
|
|
83
|
+
const visibility = RESOLVERS.visibility(service, appConfig);
|
|
84
|
+
const name = appConfig.appName?.replace(/[^a-zA-Z0-9]/g, "");
|
|
85
|
+
const packages = createPackages(appConfig).map((pkg) => pkg.ordId);
|
|
86
|
+
const suffix = visibility === RESOURCE_VISIBILITY.public ? "" : `-${visibility}`;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
placeholders.replace(service["@ORD.Extensions.partOfPackage"], { type: "package", namespace: namespace }) ??
|
|
90
|
+
[`${namespace}:package:${name}-event${suffix}:v1`, `${namespace}:package:${name}:v1`] //
|
|
91
|
+
.find((candidate) => packages.includes(candidate))
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
resourceDefinitions: (service, appConfig) => {
|
|
95
|
+
const name = service.name;
|
|
96
|
+
const ordId = RESOLVERS.ordId(service, appConfig);
|
|
97
|
+
const accessStrategies = appConfig.accessStrategies;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
service["@ORD.Extensions.resourceDefinitions"] ?? [
|
|
101
|
+
{
|
|
102
|
+
type: "asyncapi-v2",
|
|
103
|
+
mediaType: `application/json`,
|
|
104
|
+
url: `/ord/v1/${ordId}/${name}.asyncapi2.json`,
|
|
105
|
+
accessStrategies: accessStrategies,
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* This is a template function to create Event Resource object.
|
|
114
|
+
* There can be only one event resource per service because all events are using the same protocol, they are always Cloud Events.
|
|
115
|
+
* Properties of an event resource can be overwritten by the ORD extensions. Example: visibility.
|
|
116
|
+
* Ensures proper visibility compliance by checking associated EntityTypes.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} service The definition of the service
|
|
119
|
+
* @param {object} appConfig - The application configuration.
|
|
120
|
+
* @returns {object} An object representing the Event Resource.
|
|
121
|
+
*/
|
|
122
|
+
function createEventResourceTemplate(service, appConfig) {
|
|
123
|
+
return prune({
|
|
124
|
+
releaseStatus: "active",
|
|
125
|
+
extensible: { supported: "no" },
|
|
126
|
+
lastUpdate: appConfig.lastUpdate,
|
|
127
|
+
shortDescription: `${service.name} event resource`,
|
|
128
|
+
|
|
129
|
+
version: RESOLVERS.version(service),
|
|
130
|
+
ordId: RESOLVERS.ordId(service, appConfig),
|
|
131
|
+
title: RESOLVERS.title(service, appConfig),
|
|
132
|
+
description: RESOLVERS.description(service),
|
|
133
|
+
visibility: RESOLVERS.visibility(service, appConfig),
|
|
134
|
+
partOfGroups: RESOLVERS.partOfGroups(service, appConfig),
|
|
135
|
+
partOfPackage: RESOLVERS.partOfPackage(service, appConfig),
|
|
136
|
+
exposedEntityTypes: RESOLVERS.exposedEntityTypes(service, appConfig),
|
|
137
|
+
resourceDefinitions: RESOLVERS.resourceDefinitions(service, appConfig),
|
|
138
|
+
|
|
139
|
+
..._.omit(readORDExtensions(service), Object.keys(RESOLVERS)),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createEventResources(appConfig) {
|
|
144
|
+
const services = _.uniqBy(
|
|
145
|
+
Object.values(appConfig.csn.definitions)
|
|
146
|
+
.filter((definition) => definition.kind === CDS_ELEMENT_KIND.event)
|
|
147
|
+
.filter((definition) => isValidService(definition["_service"]))
|
|
148
|
+
.map((definition) => definition["_service"]),
|
|
149
|
+
"name",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return services
|
|
153
|
+
.map((service) => createEventResourceTemplate(service, appConfig))
|
|
154
|
+
.filter((resource) => resource.visibility !== RESOURCE_VISIBILITY.private);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
createEventResources,
|
|
159
|
+
createEventResourceTemplate,
|
|
160
|
+
RESOLVERS,
|
|
161
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const defaults = require("../defaults");
|
|
2
|
+
const { RESOURCE_VISIBILITY } = require("../constants");
|
|
3
|
+
const { resolveServiceName, isValidService, resolveVisibility } = require("../common/utils");
|
|
4
|
+
|
|
5
|
+
const RESOLVERS = Object.freeze({
|
|
6
|
+
title: (service) => {
|
|
7
|
+
return (
|
|
8
|
+
service["@ORD.Extensions.title"] ??
|
|
9
|
+
`${service.name
|
|
10
|
+
.split(".")
|
|
11
|
+
.pop()
|
|
12
|
+
.replace(/Service(.*)$/, "")} Service`
|
|
13
|
+
);
|
|
14
|
+
},
|
|
15
|
+
groupId: (service, appConfig) => {
|
|
16
|
+
const namespace = appConfig.ordNamespace;
|
|
17
|
+
const name = resolveServiceName(appConfig, service);
|
|
18
|
+
|
|
19
|
+
return `${defaults.groupTypeId}:${namespace}:${name}`;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This is a template function to create group object of a service for groups array in ORD doc.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} service The definition of the service
|
|
27
|
+
* @param {object} appConfig - The application configuration.
|
|
28
|
+
* @returns {Object} A group object.
|
|
29
|
+
*/
|
|
30
|
+
function createGroupsTemplateForService(service, appConfig) {
|
|
31
|
+
return {
|
|
32
|
+
groupTypeId: defaults.groupTypeId,
|
|
33
|
+
|
|
34
|
+
title: RESOLVERS.title(service),
|
|
35
|
+
groupId: RESOLVERS.groupId(service, appConfig),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createGroups(appConfig) {
|
|
40
|
+
return Object.values(appConfig.csn.definitions)
|
|
41
|
+
.filter((definition) => isValidService(definition))
|
|
42
|
+
.filter((service) => resolveVisibility(appConfig, service) !== RESOURCE_VISIBILITY.private)
|
|
43
|
+
.flatMap((service) => createGroupsTemplateForService(service, appConfig));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
createGroups,
|
|
48
|
+
createGroupsTemplateForService,
|
|
49
|
+
RESOLVERS,
|
|
50
|
+
};
|