@cap-js/ord 1.4.3 → 1.4.5
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 +8 -0
- package/lib/constants.js +47 -0
- package/lib/integrationDependency.js +127 -0
- package/lib/interopCsn.js +6 -0
- package/lib/metaData.js +21 -6
- package/lib/ord-service.js +8 -2
- package/lib/ord.js +13 -7
- package/lib/protocol-resolver.js +138 -0
- package/lib/templates.js +58 -63
- package/package.json +9 -5
package/lib/build.js
CHANGED
|
@@ -87,6 +87,14 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async build() {
|
|
90
|
+
// @cap-js/graphql registers protocols and compile targets at runtime via cds-plugin.js,
|
|
91
|
+
// but cds build rebuilds cds.env afterwards, losing both registrations.
|
|
92
|
+
// Re-apply so endpoints4() and cds.compile.to.graphql work during build.
|
|
93
|
+
if ("@cap-js/graphql" in cds.env.plugins && !cds.env.protocols?.graphql) {
|
|
94
|
+
cds.env.protocols.graphql = { path: "/graphql", impl: "@cap-js/graphql" };
|
|
95
|
+
require("@cap-js/graphql/lib/api").registerCompileTargets();
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
try {
|
|
91
99
|
const model = await this.model();
|
|
92
100
|
const ordDocument = ord(model);
|
package/lib/constants.js
CHANGED
|
@@ -53,6 +53,7 @@ const COMPILER_TYPES = Object.freeze({
|
|
|
53
53
|
edmx: "edmx",
|
|
54
54
|
csn: "csn",
|
|
55
55
|
mcp: "mcp",
|
|
56
|
+
graphql: "graphql",
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
const CONTENT_MERGE_KEY = "ordId";
|
|
@@ -117,6 +118,45 @@ const SEM_VERSION_REGEX =
|
|
|
117
118
|
|
|
118
119
|
const MCP_CUSTOM_TYPE = "sap:mcp-server-card:v0";
|
|
119
120
|
|
|
121
|
+
const EXTERNAL_DP_ORD_ID_ANNOTATION = "@cds.dp.ordId";
|
|
122
|
+
|
|
123
|
+
const EXTERNAL_SERVICE_ANNOTATION = "@cds.external";
|
|
124
|
+
|
|
125
|
+
const INTEGRATION_DEPENDENCY_RESOURCE_NAME = "externalDependencies";
|
|
126
|
+
|
|
127
|
+
// ORD apiProtocol values
|
|
128
|
+
const ORD_API_PROTOCOL = Object.freeze({
|
|
129
|
+
ODATA_V4: "odata-v4",
|
|
130
|
+
ODATA_V2: "odata-v2",
|
|
131
|
+
REST: "rest",
|
|
132
|
+
GRAPHQL: "graphql",
|
|
133
|
+
SAP_INA: "sap-ina-api-v1",
|
|
134
|
+
SAP_DATA_SUBSCRIPTION: "sap.dp:data-subscription-api:v1",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Mapping from CAP protocol kind to ORD apiProtocol
|
|
138
|
+
// CAP may return 'odata', 'odata-v4', 'rest', 'graphql', etc.
|
|
139
|
+
const CAP_TO_ORD_PROTOCOL_MAP = Object.freeze({
|
|
140
|
+
"odata": ORD_API_PROTOCOL.ODATA_V4,
|
|
141
|
+
"odata-v4": ORD_API_PROTOCOL.ODATA_V4,
|
|
142
|
+
"odata-v2": ORD_API_PROTOCOL.ODATA_V2,
|
|
143
|
+
"rest": ORD_API_PROTOCOL.REST,
|
|
144
|
+
"graphql": ORD_API_PROTOCOL.GRAPHQL,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Protocols that ORD supports but CAP may not recognize (endpoints4 returns [])
|
|
148
|
+
// These need special handling in the ORD plugin
|
|
149
|
+
const ORD_ONLY_PROTOCOLS = Object.freeze({
|
|
150
|
+
ina: {
|
|
151
|
+
apiProtocol: ORD_API_PROTOCOL.SAP_INA,
|
|
152
|
+
hasEntryPoints: false,
|
|
153
|
+
hasResourceDefinitions: false,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Protocols that the ORD plugin cannot currently generate definitions for
|
|
158
|
+
const PLUGIN_UNSUPPORTED_PROTOCOLS = Object.freeze([]);
|
|
159
|
+
|
|
120
160
|
// CF mTLS Error Reasons
|
|
121
161
|
const CF_MTLS_ERROR_REASON = Object.freeze({
|
|
122
162
|
NO_HEADERS: "NO_HEADERS",
|
|
@@ -146,6 +186,7 @@ module.exports = {
|
|
|
146
186
|
BASIC_AUTH_HEADER_KEY,
|
|
147
187
|
BUILD_DEFAULT_PATH,
|
|
148
188
|
BLOCKED_SERVICE_NAME,
|
|
189
|
+
CAP_TO_ORD_PROTOCOL_MAP,
|
|
149
190
|
CDS_ELEMENT_KIND,
|
|
150
191
|
CF_MTLS_HEADERS,
|
|
151
192
|
COMPILER_TYPES,
|
|
@@ -155,17 +196,23 @@ module.exports = {
|
|
|
155
196
|
DATA_PRODUCT_TYPE,
|
|
156
197
|
DESCRIPTION_PREFIX,
|
|
157
198
|
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
199
|
+
EXTERNAL_DP_ORD_ID_ANNOTATION,
|
|
200
|
+
EXTERNAL_SERVICE_ANNOTATION,
|
|
201
|
+
INTEGRATION_DEPENDENCY_RESOURCE_NAME,
|
|
158
202
|
LEVEL,
|
|
159
203
|
MCP_CUSTOM_TYPE,
|
|
160
204
|
OPEN_RESOURCE_DISCOVERY_VERSION,
|
|
161
205
|
OPENAPI_SERVERS_ANNOTATION,
|
|
162
206
|
ORD_ACCESS_STRATEGY,
|
|
207
|
+
ORD_API_PROTOCOL,
|
|
163
208
|
ORD_DOCUMENT_FILE_NAME,
|
|
164
209
|
ORD_EXTENSIONS_PREFIX,
|
|
165
210
|
ORD_ODM_ENTITY_NAME_ANNOTATION,
|
|
166
211
|
ORD_EXISTING_PRODUCT_PROPERTY,
|
|
212
|
+
ORD_ONLY_PROTOCOLS,
|
|
167
213
|
ORD_RESOURCE_TYPE,
|
|
168
214
|
ORD_SERVICE_NAME,
|
|
215
|
+
PLUGIN_UNSUPPORTED_PROTOCOLS,
|
|
169
216
|
RESOURCE_VISIBILITY,
|
|
170
217
|
ALLOWED_VISIBILITY,
|
|
171
218
|
IMPLEMENTATIONSTANDARD_VERSIONS,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const {
|
|
2
|
+
DATA_PRODUCT_SIMPLE_ANNOTATION,
|
|
3
|
+
EXTERNAL_DP_ORD_ID_ANNOTATION,
|
|
4
|
+
EXTERNAL_SERVICE_ANNOTATION,
|
|
5
|
+
INTEGRATION_DEPENDENCY_RESOURCE_NAME,
|
|
6
|
+
ORD_RESOURCE_TYPE,
|
|
7
|
+
RESOURCE_VISIBILITY,
|
|
8
|
+
} = require("./constants");
|
|
9
|
+
const { readORDExtensions, _getPackageID } = require("./templates");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parses @cds.dp.ordId annotation to extract resource information.
|
|
13
|
+
* @param {string} ordId - e.g., "sap.sai:apiResource:Supplier:v1"
|
|
14
|
+
* @returns {Object} { namespace, resourceType, resourceName, version }
|
|
15
|
+
*/
|
|
16
|
+
function parseDataProductOrdId(ordId) {
|
|
17
|
+
const [namespace, resourceType, resourceName, version = "v1"] = ordId.split(":");
|
|
18
|
+
return { namespace, resourceType, resourceName, version };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a CSN definition is an external Data Product.
|
|
23
|
+
* @param {Object} definition - CSN definition object
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isExternalDataProduct(definition) {
|
|
27
|
+
return !!(
|
|
28
|
+
definition.kind === "service" &&
|
|
29
|
+
definition[EXTERNAL_SERVICE_ANNOTATION] &&
|
|
30
|
+
definition[DATA_PRODUCT_SIMPLE_ANNOTATION] &&
|
|
31
|
+
definition[EXTERNAL_DP_ORD_ID_ANNOTATION]
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Collects external services from CSN definitions.
|
|
37
|
+
* Returns a flat list of external services (one per service, no namespace grouping).
|
|
38
|
+
* @param {Object} csn - The CSN definitions object
|
|
39
|
+
* @returns {Array} Array of external service objects
|
|
40
|
+
*/
|
|
41
|
+
function collectExternalServices(csn) {
|
|
42
|
+
const externalServices = [];
|
|
43
|
+
|
|
44
|
+
for (const [serviceName, definition] of Object.entries(csn.definitions)) {
|
|
45
|
+
if (!isExternalDataProduct(definition)) continue;
|
|
46
|
+
|
|
47
|
+
const dpOrdId = definition[EXTERNAL_DP_ORD_ID_ANNOTATION];
|
|
48
|
+
const { resourceType, version } = parseDataProductOrdId(dpOrdId);
|
|
49
|
+
|
|
50
|
+
// Only support apiResource for now
|
|
51
|
+
if (resourceType !== "apiResource") continue;
|
|
52
|
+
|
|
53
|
+
externalServices.push({
|
|
54
|
+
serviceName,
|
|
55
|
+
ordId: dpOrdId,
|
|
56
|
+
minVersion: version.replace("v", "") + ".0.0",
|
|
57
|
+
definition,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return externalServices;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a single IntegrationDependency with one aspect per external service.
|
|
66
|
+
* @param {Array} externalServices - Array of external service objects
|
|
67
|
+
* @param {Object} appConfig - The application configuration
|
|
68
|
+
* @param {Array} packageIds - The available package identifiers
|
|
69
|
+
* @returns {Object} IntegrationDependency object
|
|
70
|
+
*/
|
|
71
|
+
function createIntegrationDependency(externalServices, appConfig, packageIds) {
|
|
72
|
+
const packageId = _getPackageID(
|
|
73
|
+
appConfig.ordNamespace,
|
|
74
|
+
packageIds,
|
|
75
|
+
ORD_RESOURCE_TYPE.integrationDependency,
|
|
76
|
+
RESOURCE_VISIBILITY.public,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Create one aspect per external service
|
|
80
|
+
const aspects = externalServices.map((service) => {
|
|
81
|
+
const ordExtensions = readORDExtensions(service.definition || {});
|
|
82
|
+
return {
|
|
83
|
+
title: service.serviceName,
|
|
84
|
+
mandatory: false,
|
|
85
|
+
apiResources: [{ ordId: service.ordId, minVersion: service.minVersion }],
|
|
86
|
+
...ordExtensions, // Allow customization via @ORD.Extensions on the service
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Read IntegrationDependency level config from cdsrc
|
|
91
|
+
const integrationDepConfig = appConfig.env?.integrationDependency || {};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
ordId: `${appConfig.ordNamespace}:${ORD_RESOURCE_TYPE.integrationDependency}:${INTEGRATION_DEPENDENCY_RESOURCE_NAME}:v1`,
|
|
95
|
+
title: "External Dependencies",
|
|
96
|
+
version: "1.0.0",
|
|
97
|
+
releaseStatus: "active",
|
|
98
|
+
visibility: RESOURCE_VISIBILITY.public,
|
|
99
|
+
mandatory: false,
|
|
100
|
+
partOfPackage: packageId,
|
|
101
|
+
aspects,
|
|
102
|
+
...integrationDepConfig, // Allow customization via cdsrc
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generates a single IntegrationDependency for all external services.
|
|
108
|
+
* @param {Object} csn - The CSN definitions object
|
|
109
|
+
* @param {Object} appConfig - The application configuration
|
|
110
|
+
* @param {Array} packageIds - The available package identifiers
|
|
111
|
+
* @returns {Array} Array containing single IntegrationDependency or empty array
|
|
112
|
+
*/
|
|
113
|
+
function getIntegrationDependencies(csn, appConfig, packageIds) {
|
|
114
|
+
const externalServices = collectExternalServices(csn);
|
|
115
|
+
if (externalServices.length === 0) return [];
|
|
116
|
+
|
|
117
|
+
return [createIntegrationDependency(externalServices, appConfig, packageIds)];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
getIntegrationDependencies,
|
|
122
|
+
// Exported for testing
|
|
123
|
+
collectExternalServices,
|
|
124
|
+
createIntegrationDependency,
|
|
125
|
+
parseDataProductOrdId,
|
|
126
|
+
isExternalDataProduct,
|
|
127
|
+
};
|
package/lib/interopCsn.js
CHANGED
|
@@ -117,6 +117,12 @@ function map_annotations(csn) {
|
|
|
117
117
|
delete o[oldA];
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
// delete all @assert.unique annotations
|
|
122
|
+
let assertUniqueAnnos = Object.keys(o).filter(x => x.startsWith('@assert.unique'))
|
|
123
|
+
for (let a of assertUniqueAnnos) {
|
|
124
|
+
delete o[a]
|
|
125
|
+
}
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
|
package/lib/metaData.js
CHANGED
|
@@ -46,7 +46,7 @@ const getMetadata = async (url, model = null) => {
|
|
|
46
46
|
|
|
47
47
|
responseFile = openapi(csn, openapiOptions);
|
|
48
48
|
} catch (error) {
|
|
49
|
-
Logger.error(
|
|
49
|
+
Logger.error(`OpenApi error for service ${serviceName} - ${error.message}`);
|
|
50
50
|
throw error;
|
|
51
51
|
}
|
|
52
52
|
break;
|
|
@@ -54,7 +54,7 @@ const getMetadata = async (url, model = null) => {
|
|
|
54
54
|
try {
|
|
55
55
|
responseFile = asyncapi(csn, { ...options, ...(compileOptions?.asyncapi || {}) });
|
|
56
56
|
} catch (error) {
|
|
57
|
-
Logger.error(
|
|
57
|
+
Logger.error(`AsyncApi error for service ${serviceName} - ${error.message}`);
|
|
58
58
|
throw error;
|
|
59
59
|
}
|
|
60
60
|
break;
|
|
@@ -64,7 +64,7 @@ const getMetadata = async (url, model = null) => {
|
|
|
64
64
|
let effCsn = cdsc.for.effective(csn, opt_eff);
|
|
65
65
|
responseFile = interopCSN(effCsn);
|
|
66
66
|
} catch (error) {
|
|
67
|
-
Logger.error(
|
|
67
|
+
Logger.error(`Csn error for service ${serviceName} - ${error.message}`);
|
|
68
68
|
throw error;
|
|
69
69
|
}
|
|
70
70
|
break;
|
|
@@ -72,7 +72,7 @@ const getMetadata = async (url, model = null) => {
|
|
|
72
72
|
try {
|
|
73
73
|
responseFile = await cds.compile(csn).to["edmx"]({ ...options, ...(compileOptions?.edmx || {}) });
|
|
74
74
|
} catch (error) {
|
|
75
|
-
Logger.error(
|
|
75
|
+
Logger.error(`Edmx error for service ${serviceName} - ${error.message}`);
|
|
76
76
|
throw error;
|
|
77
77
|
}
|
|
78
78
|
break;
|
|
@@ -88,13 +88,28 @@ const getMetadata = async (url, model = null) => {
|
|
|
88
88
|
// Extract only the MCP content, not the ORD metadata
|
|
89
89
|
responseFile = mcpResult.mcp;
|
|
90
90
|
} catch (error) {
|
|
91
|
-
Logger.error(
|
|
91
|
+
Logger.error(`MCP server definition error - ${error.message}`);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case COMPILER_TYPES.graphql:
|
|
96
|
+
try {
|
|
97
|
+
const { generateSchema4 } = require("@cap-js/graphql/lib/schema");
|
|
98
|
+
const { lexicographicSortSchema, printSchema } = require("graphql");
|
|
99
|
+
const linked = cds.linked(csn);
|
|
100
|
+
const srv = new cds.ApplicationService(serviceName, linked);
|
|
101
|
+
let schema = generateSchema4({ [serviceName]: srv });
|
|
102
|
+
schema = lexicographicSortSchema(schema);
|
|
103
|
+
responseFile = printSchema(schema);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
Logger.error("GraphQL SDL error:", error.message);
|
|
92
106
|
throw error;
|
|
93
107
|
}
|
|
94
108
|
break;
|
|
95
109
|
}
|
|
96
110
|
return {
|
|
97
|
-
contentType:
|
|
111
|
+
contentType:
|
|
112
|
+
compilerType === "graphql" ? "text/plain" : `application/${compilerType === "edmx" ? "xml" : "json"}`,
|
|
98
113
|
response: responseFile,
|
|
99
114
|
};
|
|
100
115
|
};
|
package/lib/ord-service.js
CHANGED
|
@@ -31,7 +31,8 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
|
31
31
|
return res.status(404).send("404 Not Found");
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// Handler for metadata requests (oas3, edmx, csn, etc.)
|
|
35
|
+
const metadataHandler = async (req, res) => {
|
|
35
36
|
try {
|
|
36
37
|
const { contentType, response } = await getMetadata(req.url);
|
|
37
38
|
return res.status(200).contentType(contentType).send(response);
|
|
@@ -39,7 +40,12 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
|
39
40
|
Logger.error(error, "Error while processing the resource definition document");
|
|
40
41
|
return res.status(500).send(error.message);
|
|
41
42
|
}
|
|
42
|
-
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Use separate routes instead of optional parameters for path-to-regexp v8 compatibility
|
|
46
|
+
cds.app.get(`/ord/v1/:ordId/:service`, authMiddleware, metadataHandler);
|
|
47
|
+
cds.app.get(`/ord/v1/:ordId`, authMiddleware, metadataHandler);
|
|
48
|
+
cds.app.get(`/ord/v1`, authMiddleware, metadataHandler);
|
|
43
49
|
|
|
44
50
|
return super.init();
|
|
45
51
|
}
|
package/lib/ord.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
createMCPAPIResourceTemplate,
|
|
15
15
|
_propagateORDVisibility,
|
|
16
16
|
} = require("./templates");
|
|
17
|
+
const { getIntegrationDependencies } = require("./integrationDependency");
|
|
17
18
|
const { extendCustomORDContentIfExists } = require("./extendOrdWithCustom");
|
|
18
19
|
const { getRFC3339Date } = require("./date");
|
|
19
20
|
const { createAuthConfig } = require("./auth/authentication");
|
|
@@ -330,18 +331,17 @@ function _filterUnusedPackages(ordDocument) {
|
|
|
330
331
|
|
|
331
332
|
const usedPackageIds = new Set();
|
|
332
333
|
|
|
333
|
-
ordDocument.apiResources?.forEach((api) => usedPackageIds
|
|
334
|
-
ordDocument.eventResources?.forEach((event) => usedPackageIds
|
|
335
|
-
ordDocument.dataProducts?.forEach((dp) => usedPackageIds
|
|
334
|
+
ordDocument.apiResources?.forEach((api) => usedPackageIds.add(api.partOfPackage));
|
|
335
|
+
ordDocument.eventResources?.forEach((event) => usedPackageIds.add(event.partOfPackage));
|
|
336
|
+
ordDocument.dataProducts?.forEach((dp) => usedPackageIds.add(dp.partOfPackage));
|
|
337
|
+
ordDocument.integrationDependencies?.forEach((dep) => usedPackageIds.add(dep.partOfPackage));
|
|
336
338
|
ordDocument.entityTypes?.forEach((et) => {
|
|
337
|
-
if (et
|
|
339
|
+
if (et?.partOfPackage) {
|
|
338
340
|
usedPackageIds.add(et.partOfPackage);
|
|
339
341
|
}
|
|
340
342
|
});
|
|
341
343
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return filteredPackages;
|
|
344
|
+
return ordDocument.packages.filter((pkg) => usedPackageIds.has(pkg.ordId));
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
module.exports = (csn) => {
|
|
@@ -375,6 +375,12 @@ module.exports = (csn) => {
|
|
|
375
375
|
ordDocument.eventResources = eventResources;
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
|
+
|
|
379
|
+
const integrationDependencies = getIntegrationDependencies(linkedCsn, appConfig, packageIds);
|
|
380
|
+
if (integrationDependencies.length) {
|
|
381
|
+
ordDocument.integrationDependencies = integrationDependencies;
|
|
382
|
+
}
|
|
383
|
+
|
|
378
384
|
ordDocument = extendCustomORDContentIfExists(appConfig, ordDocument);
|
|
379
385
|
ordDocument.packages = _filterUnusedPackages(ordDocument);
|
|
380
386
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
|
+
const {
|
|
3
|
+
CAP_TO_ORD_PROTOCOL_MAP,
|
|
4
|
+
ORD_ONLY_PROTOCOLS,
|
|
5
|
+
ORD_API_PROTOCOL,
|
|
6
|
+
PLUGIN_UNSUPPORTED_PROTOCOLS,
|
|
7
|
+
} = require("./constants");
|
|
8
|
+
const Logger = require("./logger");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Gets CAP endpoints for a service using CDS endpoints4().
|
|
12
|
+
*
|
|
13
|
+
* @param {string} serviceName The service name.
|
|
14
|
+
* @param {Object} srvDefinition The service definition object.
|
|
15
|
+
* @returns {Array} Raw endpoints from CDS.
|
|
16
|
+
*/
|
|
17
|
+
function _getCapEndpoints(serviceName, srvDefinition) {
|
|
18
|
+
const srvObj = { name: serviceName, definition: srvDefinition };
|
|
19
|
+
return cds.service.protocols.endpoints4(srvObj);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reads the explicit @protocol annotation from service definition.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} srvDefinition The service definition object.
|
|
26
|
+
* @returns {string[]} Array of protocol names, or empty array if not explicitly set.
|
|
27
|
+
*/
|
|
28
|
+
function _getExplicitProtocol(srvDefinition) {
|
|
29
|
+
const protocol = srvDefinition["@protocol"];
|
|
30
|
+
if (!protocol) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return Array.isArray(protocol) ? protocol : [protocol];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves protocol for ORD API Resource generation.
|
|
38
|
+
*
|
|
39
|
+
* Design Principles:
|
|
40
|
+
* - explicit protocol is the "master switch" for all decisions
|
|
41
|
+
* - Rule A: Explicit protocol + empty endpoints → don't fall back to OData
|
|
42
|
+
* - Rule B: Only fallback to OData when no explicit protocol
|
|
43
|
+
* - Rule C: Never produce [null] in entryPoints
|
|
44
|
+
*
|
|
45
|
+
* @param {string} serviceName The service name.
|
|
46
|
+
* @param {Object} srvDefinition The service definition object.
|
|
47
|
+
* @param {Object} options Configuration options.
|
|
48
|
+
* @param {Function} options.isPrimaryDataProduct Strategy function to check if service is primary data product.
|
|
49
|
+
* @returns {Array} Array with single {apiProtocol, entryPoints, hasResourceDefinitions} object, or empty array.
|
|
50
|
+
*/
|
|
51
|
+
function resolveApiResourceProtocol(serviceName, srvDefinition, options = {}) {
|
|
52
|
+
const { isPrimaryDataProduct = () => false } = options;
|
|
53
|
+
|
|
54
|
+
// 1. Primary Data Product - early return
|
|
55
|
+
if (isPrimaryDataProduct(srvDefinition)) {
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
apiProtocol: ORD_API_PROTOCOL.SAP_DATA_SUBSCRIPTION,
|
|
59
|
+
entryPoints: [],
|
|
60
|
+
hasResourceDefinitions: true,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const capEndpoints = _getCapEndpoints(serviceName, srvDefinition);
|
|
66
|
+
const ordProtocols = [];
|
|
67
|
+
for (const endpoint of capEndpoints) {
|
|
68
|
+
if (PLUGIN_UNSUPPORTED_PROTOCOLS.includes(endpoint.kind)) {
|
|
69
|
+
Logger.warn(
|
|
70
|
+
`Protocol '${endpoint.kind}' is supported by ORD but this plugin cannot generate its resource definitions yet.`,
|
|
71
|
+
);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const apiProtocol = CAP_TO_ORD_PROTOCOL_MAP[endpoint.kind] ?? endpoint.kind;
|
|
76
|
+
if (apiProtocol) {
|
|
77
|
+
ordProtocols.push({
|
|
78
|
+
apiProtocol,
|
|
79
|
+
entryPoints: endpoint.path ? [endpoint.path] : [],
|
|
80
|
+
hasResourceDefinitions: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const explicit = _getExplicitProtocol(srvDefinition);
|
|
86
|
+
for (const protocol of explicit) {
|
|
87
|
+
// 2. Handle explicit protocol
|
|
88
|
+
// 2a. Check if it's an ORD-only protocol (e.g., INA)
|
|
89
|
+
if (ORD_ONLY_PROTOCOLS[protocol]) {
|
|
90
|
+
const config = ORD_ONLY_PROTOCOLS[protocol];
|
|
91
|
+
const path = config.hasEntryPoints ? cds.service.protocols.path4(srvDefinition) : null;
|
|
92
|
+
ordProtocols.push({
|
|
93
|
+
apiProtocol: config.apiProtocol,
|
|
94
|
+
entryPoints: path ? [path] : [],
|
|
95
|
+
hasResourceDefinitions: config.hasResourceDefinitions,
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2b. Check if it's a plugin-unsupported protocol
|
|
101
|
+
if (PLUGIN_UNSUPPORTED_PROTOCOLS.includes(protocol)) {
|
|
102
|
+
Logger.warn(
|
|
103
|
+
`Protocol '${protocol}' is supported by ORD but this plugin cannot generate its resource definitions yet.`,
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 4. Handle explicit protocol with no CAP endpoint (Rule A)
|
|
109
|
+
if (!ordProtocols.some((p) => p.apiProtocol === protocol) && !CAP_TO_ORD_PROTOCOL_MAP[protocol]) {
|
|
110
|
+
Logger.warn(`Unknown protocol '${protocol}' is not supported, and skipped for service '${serviceName}'.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// no protocol found via CAP endpoints, but explicit protocol(s) defined → Rule A: don't fall back to OData, return empty array
|
|
115
|
+
if (ordProtocols.length === 0 && explicit.length > 0) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (ordProtocols.length > 0) {
|
|
120
|
+
return ordProtocols;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 5. No explicit protocol and no CAP endpoint - fallback to OData (Rule B)
|
|
124
|
+
const path = cds.service.protocols.path4(srvDefinition);
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
apiProtocol: ORD_API_PROTOCOL.ODATA_V4,
|
|
128
|
+
entryPoints: path ? [path] : [],
|
|
129
|
+
hasResourceDefinitions: true,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
resolveApiResourceProtocol,
|
|
136
|
+
// Exported for testing
|
|
137
|
+
_getExplicitProtocol,
|
|
138
|
+
};
|
package/lib/templates.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const cds = require("@sap/cds");
|
|
2
1
|
const { hasSAPPolicyLevel } = require("./utils");
|
|
3
2
|
const { isMCPPluginReady } = require("./mcpAdapter");
|
|
4
3
|
const defaults = require("./defaults");
|
|
@@ -21,9 +20,11 @@ const {
|
|
|
21
20
|
SHORT_DESCRIPTION_PREFIX,
|
|
22
21
|
CONTENT_MERGE_KEY,
|
|
23
22
|
CDS_ELEMENT_KIND,
|
|
23
|
+
ORD_API_PROTOCOL,
|
|
24
24
|
} = require("./constants");
|
|
25
25
|
const Logger = require("./logger");
|
|
26
26
|
const { ensureAccessStrategies } = require("./access-strategies");
|
|
27
|
+
const { resolveApiResourceProtocol } = require("./protocol-resolver");
|
|
27
28
|
|
|
28
29
|
function unflatten(flattedObject) {
|
|
29
30
|
let result = {};
|
|
@@ -44,42 +45,6 @@ function readORDExtensions(model) {
|
|
|
44
45
|
return unflatten(ordExtensions);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
-
* Reads the service definition and returns an array of entryPoint paths.
|
|
49
|
-
*
|
|
50
|
-
* @param {string} srv The service definition name.
|
|
51
|
-
* @param {Object} srvDefinition The service definition object.
|
|
52
|
-
* @returns {Array} An array containing paths and it's kind.
|
|
53
|
-
*/
|
|
54
|
-
const _generatePaths = (srv, srvDefinition) => {
|
|
55
|
-
const srvObj = { name: srv, definition: srvDefinition };
|
|
56
|
-
const protocols = cds.service.protocols;
|
|
57
|
-
|
|
58
|
-
const paths = protocols.endpoints4(srvObj);
|
|
59
|
-
|
|
60
|
-
//TODO: check graphql replication in paths object and re-visit logic
|
|
61
|
-
//removing instances of graphql protocol from paths
|
|
62
|
-
for (var index = paths.length - 1; index >= 0; index--) {
|
|
63
|
-
if (paths[index].kind === "graphql") {
|
|
64
|
-
Logger.warn("Graphql protocol is not supported.");
|
|
65
|
-
paths.splice(index, 1);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
//putting OData as default in case of non-supported protocol
|
|
70
|
-
if (paths.length === 0) {
|
|
71
|
-
if (isPrimaryDataProductService(srvDefinition)) {
|
|
72
|
-
// Data product services use REST protocol, not OData
|
|
73
|
-
paths.push({ kind: "rest", path: protocols.path4(srvDefinition) });
|
|
74
|
-
} else {
|
|
75
|
-
srvDefinition["@odata"] = true;
|
|
76
|
-
paths.push({ kind: "odata", path: protocols.path4(srvDefinition) });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return paths;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
48
|
/**
|
|
84
49
|
* This is a template function to create item of entityTypeMappings array.
|
|
85
50
|
*
|
|
@@ -321,9 +286,17 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
321
286
|
const visibility = _handleVisibility(ordExtensions, serviceDefinition, appConfig.env?.defaultVisibility);
|
|
322
287
|
const packageId = _getPackageID(appConfig.ordNamespace, packageIds, ORD_RESOURCE_TYPE.api, visibility);
|
|
323
288
|
|
|
324
|
-
const
|
|
289
|
+
const protocolResults = resolveApiResourceProtocol(serviceName, serviceDefinition, {
|
|
290
|
+
isPrimaryDataProduct: isPrimaryDataProductService,
|
|
291
|
+
});
|
|
325
292
|
const apiResources = [];
|
|
326
293
|
|
|
294
|
+
// If no protocols were generated, skip this service
|
|
295
|
+
if (protocolResults.length === 0) {
|
|
296
|
+
Logger.info(`No supported protocols for service '${serviceName}', skipping API resource generation.`);
|
|
297
|
+
return apiResources;
|
|
298
|
+
}
|
|
299
|
+
|
|
327
300
|
// Handle version suffix extraction for primary data product services
|
|
328
301
|
let cleanServiceName,
|
|
329
302
|
version,
|
|
@@ -352,16 +325,48 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
352
325
|
|
|
353
326
|
const ordId = `${appConfig.ordNamespace}:apiResource:${cleanServiceName}:${version}`;
|
|
354
327
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
328
|
+
protocolResults.forEach((protocolResult) => {
|
|
329
|
+
const { apiProtocol, entryPoints, hasResourceDefinitions } = protocolResult;
|
|
330
|
+
|
|
331
|
+
// Build resource definitions based on protocol
|
|
332
|
+
let resourceDefinitions = [];
|
|
333
|
+
if (hasResourceDefinitions) {
|
|
334
|
+
if (apiProtocol === ORD_API_PROTOCOL.SAP_DATA_SUBSCRIPTION) {
|
|
335
|
+
// Data product services use CSN
|
|
336
|
+
resourceDefinitions = [
|
|
337
|
+
_getResourceDefinition(
|
|
338
|
+
"sap-csn-interop-effective-v1",
|
|
339
|
+
"json",
|
|
340
|
+
ordId,
|
|
341
|
+
serviceName,
|
|
342
|
+
"csn.json",
|
|
343
|
+
accessStrategies,
|
|
344
|
+
),
|
|
345
|
+
];
|
|
346
|
+
} else if (apiProtocol === ORD_API_PROTOCOL.REST) {
|
|
347
|
+
// REST only has OpenAPI, no EDMX
|
|
348
|
+
resourceDefinitions = [
|
|
349
|
+
_getResourceDefinition("openapi-v3", "json", ordId, serviceName, "oas3.json", accessStrategies),
|
|
350
|
+
];
|
|
351
|
+
} else if (apiProtocol === ORD_API_PROTOCOL.GRAPHQL) {
|
|
352
|
+
// GraphQL only has GraphQL SDL
|
|
353
|
+
resourceDefinitions = [
|
|
354
|
+
{
|
|
355
|
+
type: "graphql-sdl",
|
|
356
|
+
mediaType: "text/plain",
|
|
357
|
+
url: `/ord/v1/${ordId}/${serviceName}.graphql`,
|
|
358
|
+
accessStrategies: ensureAccessStrategies(accessStrategies, {
|
|
359
|
+
resourceName: `${serviceName} (graphql-sdl)`,
|
|
360
|
+
}),
|
|
361
|
+
},
|
|
362
|
+
];
|
|
363
|
+
} else {
|
|
364
|
+
// OData and others have both OpenAPI and EDMX
|
|
365
|
+
resourceDefinitions = [
|
|
366
|
+
_getResourceDefinition("openapi-v3", "json", ordId, serviceName, "oas3.json", accessStrategies),
|
|
367
|
+
_getResourceDefinition("edmx", "xml", ordId, serviceName, "edmx", accessStrategies),
|
|
368
|
+
];
|
|
369
|
+
}
|
|
365
370
|
}
|
|
366
371
|
|
|
367
372
|
const entityTypeMappings = _getEntityTypeMappings(serviceDefinition);
|
|
@@ -378,9 +383,9 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
378
383
|
partOfPackage: packageId,
|
|
379
384
|
partOfGroups: [_getGroupID(serviceDefinition, defaults.groupTypeId, appConfig)],
|
|
380
385
|
releaseStatus: "active",
|
|
381
|
-
apiProtocol
|
|
382
|
-
resourceDefinitions
|
|
383
|
-
entryPoints
|
|
386
|
+
apiProtocol,
|
|
387
|
+
resourceDefinitions,
|
|
388
|
+
entryPoints,
|
|
384
389
|
extensible: {
|
|
385
390
|
supported: "no",
|
|
386
391
|
},
|
|
@@ -389,24 +394,13 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
389
394
|
...ordExtensions,
|
|
390
395
|
};
|
|
391
396
|
|
|
397
|
+
// Special handling for data product services
|
|
392
398
|
if (isPrimaryDataProductService(serviceDefinition)) {
|
|
393
|
-
obj.apiProtocol = "sap.dp:data-subscription-api:v1";
|
|
394
399
|
obj.direction = "outbound";
|
|
395
|
-
obj.entryPoints = [];
|
|
396
400
|
if (extracted) {
|
|
397
401
|
// Overwrite partOfGroups
|
|
398
402
|
obj.partOfGroups = [`${defaults.groupTypeId}:${appConfig.ordNamespace}:${cleanServiceName}`];
|
|
399
403
|
}
|
|
400
|
-
obj.resourceDefinitions = [
|
|
401
|
-
_getResourceDefinition(
|
|
402
|
-
"sap-csn-interop-effective-v1",
|
|
403
|
-
"json",
|
|
404
|
-
ordId,
|
|
405
|
-
serviceName,
|
|
406
|
-
"csn.json",
|
|
407
|
-
accessStrategies,
|
|
408
|
-
),
|
|
409
|
-
];
|
|
410
404
|
}
|
|
411
405
|
|
|
412
406
|
if (obj.visibility !== RESOURCE_VISIBILITY.private) {
|
|
@@ -717,6 +711,7 @@ module.exports = {
|
|
|
717
711
|
createAPIResourceTemplate,
|
|
718
712
|
createEventResourceTemplate,
|
|
719
713
|
createMCPAPIResourceTemplate,
|
|
714
|
+
readORDExtensions,
|
|
720
715
|
_getPackageID,
|
|
721
716
|
_getEntityTypeMappings,
|
|
722
717
|
_getExposedEntityTypes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/ord",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"description": "CAP Plugin for generating ORD document.",
|
|
5
5
|
"repository": "cap-js/ord",
|
|
6
6
|
"author": "SAP SE (https://www.sap.com)",
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"cds:version": "cds v -i"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"
|
|
31
|
+
"@cap-js/graphql": "^0.14.0",
|
|
32
|
+
"@cap-js/sqlite": "^2",
|
|
33
|
+
"@sap/cds-dk": ">=8.9.5",
|
|
34
|
+
"eslint": "^10.0.0",
|
|
32
35
|
"express": "^4",
|
|
33
36
|
"jest": "^30.0.0",
|
|
34
37
|
"prettier": "3.8.1",
|
|
35
|
-
"supertest": "^7.0.0"
|
|
36
|
-
"@cap-js/sqlite": "^2",
|
|
37
|
-
"@sap/cds-dk": ">=8.9.5"
|
|
38
|
+
"supertest": "^7.0.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
41
|
"@sap/cds": ">=8.9.4"
|
|
@@ -49,6 +50,9 @@
|
|
|
49
50
|
"engines": {
|
|
50
51
|
"node": ">=18 <23"
|
|
51
52
|
},
|
|
53
|
+
"overrides": {
|
|
54
|
+
"@sap/cds-compiler": "6.7.3"
|
|
55
|
+
},
|
|
52
56
|
"cds": {
|
|
53
57
|
"requires": {
|
|
54
58
|
"SAP ORD Service": {
|