@cap-js/ord 1.4.4 → 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 +15 -5
- 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 +45 -41
- package/lib/templates.js +13 -0
- package/package.json +7 -6
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,12 @@ 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
|
+
|
|
120
127
|
// ORD apiProtocol values
|
|
121
128
|
const ORD_API_PROTOCOL = Object.freeze({
|
|
122
129
|
ODATA_V4: "odata-v4",
|
|
@@ -128,18 +135,19 @@ const ORD_API_PROTOCOL = Object.freeze({
|
|
|
128
135
|
});
|
|
129
136
|
|
|
130
137
|
// Mapping from CAP protocol kind to ORD apiProtocol
|
|
131
|
-
// CAP may return 'odata', 'odata-v4', 'rest', etc.
|
|
138
|
+
// CAP may return 'odata', 'odata-v4', 'rest', 'graphql', etc.
|
|
132
139
|
const CAP_TO_ORD_PROTOCOL_MAP = Object.freeze({
|
|
133
140
|
"odata": ORD_API_PROTOCOL.ODATA_V4,
|
|
134
141
|
"odata-v4": ORD_API_PROTOCOL.ODATA_V4,
|
|
135
142
|
"odata-v2": ORD_API_PROTOCOL.ODATA_V2,
|
|
136
143
|
"rest": ORD_API_PROTOCOL.REST,
|
|
144
|
+
"graphql": ORD_API_PROTOCOL.GRAPHQL,
|
|
137
145
|
});
|
|
138
146
|
|
|
139
|
-
// Protocols that ORD supports but CAP
|
|
147
|
+
// Protocols that ORD supports but CAP may not recognize (endpoints4 returns [])
|
|
140
148
|
// These need special handling in the ORD plugin
|
|
141
149
|
const ORD_ONLY_PROTOCOLS = Object.freeze({
|
|
142
|
-
|
|
150
|
+
ina: {
|
|
143
151
|
apiProtocol: ORD_API_PROTOCOL.SAP_INA,
|
|
144
152
|
hasEntryPoints: false,
|
|
145
153
|
hasResourceDefinitions: false,
|
|
@@ -147,8 +155,7 @@ const ORD_ONLY_PROTOCOLS = Object.freeze({
|
|
|
147
155
|
});
|
|
148
156
|
|
|
149
157
|
// Protocols that the ORD plugin cannot currently generate definitions for
|
|
150
|
-
|
|
151
|
-
const PLUGIN_UNSUPPORTED_PROTOCOLS = Object.freeze(["graphql"]);
|
|
158
|
+
const PLUGIN_UNSUPPORTED_PROTOCOLS = Object.freeze([]);
|
|
152
159
|
|
|
153
160
|
// CF mTLS Error Reasons
|
|
154
161
|
const CF_MTLS_ERROR_REASON = Object.freeze({
|
|
@@ -189,6 +196,9 @@ module.exports = {
|
|
|
189
196
|
DATA_PRODUCT_TYPE,
|
|
190
197
|
DESCRIPTION_PREFIX,
|
|
191
198
|
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
199
|
+
EXTERNAL_DP_ORD_ID_ANNOTATION,
|
|
200
|
+
EXTERNAL_SERVICE_ANNOTATION,
|
|
201
|
+
INTEGRATION_DEPENDENCY_RESOURCE_NAME,
|
|
192
202
|
LEVEL,
|
|
193
203
|
MCP_CUSTOM_TYPE,
|
|
194
204
|
OPEN_RESOURCE_DISCOVERY_VERSION,
|
|
@@ -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
|
|
package/lib/protocol-resolver.js
CHANGED
|
@@ -23,14 +23,14 @@ function _getCapEndpoints(serviceName, srvDefinition) {
|
|
|
23
23
|
* Reads the explicit @protocol annotation from service definition.
|
|
24
24
|
*
|
|
25
25
|
* @param {Object} srvDefinition The service definition object.
|
|
26
|
-
* @returns {string
|
|
26
|
+
* @returns {string[]} Array of protocol names, or empty array if not explicitly set.
|
|
27
27
|
*/
|
|
28
28
|
function _getExplicitProtocol(srvDefinition) {
|
|
29
29
|
const protocol = srvDefinition["@protocol"];
|
|
30
30
|
if (!protocol) {
|
|
31
|
-
return
|
|
31
|
+
return [];
|
|
32
32
|
}
|
|
33
|
-
return Array.isArray(protocol) ? protocol
|
|
33
|
+
return Array.isArray(protocol) ? protocol : [protocol];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -38,7 +38,7 @@ function _getExplicitProtocol(srvDefinition) {
|
|
|
38
38
|
*
|
|
39
39
|
* Design Principles:
|
|
40
40
|
* - explicit protocol is the "master switch" for all decisions
|
|
41
|
-
* - Rule A: Explicit protocol + empty endpoints → don't
|
|
41
|
+
* - Rule A: Explicit protocol + empty endpoints → don't fall back to OData
|
|
42
42
|
* - Rule B: Only fallback to OData when no explicit protocol
|
|
43
43
|
* - Rule C: Never produce [null] in entryPoints
|
|
44
44
|
*
|
|
@@ -62,34 +62,8 @@ function resolveApiResourceProtocol(serviceName, srvDefinition, options = {}) {
|
|
|
62
62
|
];
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const explicit = _getExplicitProtocol(srvDefinition);
|
|
66
|
-
|
|
67
|
-
// 2. Handle explicit protocol
|
|
68
|
-
if (explicit) {
|
|
69
|
-
// 2a. Check if it's an ORD-only protocol (e.g., INA)
|
|
70
|
-
if (ORD_ONLY_PROTOCOLS[explicit]) {
|
|
71
|
-
const config = ORD_ONLY_PROTOCOLS[explicit];
|
|
72
|
-
const path = config.hasEntryPoints ? cds.service.protocols.path4(srvDefinition) : null;
|
|
73
|
-
return [
|
|
74
|
-
{
|
|
75
|
-
apiProtocol: config.apiProtocol,
|
|
76
|
-
entryPoints: path ? [path] : [],
|
|
77
|
-
hasResourceDefinitions: config.hasResourceDefinitions,
|
|
78
|
-
},
|
|
79
|
-
];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 2b. Check if it's a plugin-unsupported protocol
|
|
83
|
-
if (PLUGIN_UNSUPPORTED_PROTOCOLS.includes(explicit)) {
|
|
84
|
-
Logger.warn(
|
|
85
|
-
`Protocol '${explicit}' is supported by ORD but this plugin cannot generate its resource definitions yet.`,
|
|
86
|
-
);
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 3. Try to resolve from CAP endpoints
|
|
92
65
|
const capEndpoints = _getCapEndpoints(serviceName, srvDefinition);
|
|
66
|
+
const ordProtocols = [];
|
|
93
67
|
for (const endpoint of capEndpoints) {
|
|
94
68
|
if (PLUGIN_UNSUPPORTED_PROTOCOLS.includes(endpoint.kind)) {
|
|
95
69
|
Logger.warn(
|
|
@@ -100,22 +74,52 @@ function resolveApiResourceProtocol(serviceName, srvDefinition, options = {}) {
|
|
|
100
74
|
|
|
101
75
|
const apiProtocol = CAP_TO_ORD_PROTOCOL_MAP[endpoint.kind] ?? endpoint.kind;
|
|
102
76
|
if (apiProtocol) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
];
|
|
77
|
+
ordProtocols.push({
|
|
78
|
+
apiProtocol,
|
|
79
|
+
entryPoints: endpoint.path ? [endpoint.path] : [],
|
|
80
|
+
hasResourceDefinitions: true,
|
|
81
|
+
});
|
|
110
82
|
}
|
|
111
83
|
}
|
|
112
84
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
116
|
return [];
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
if (ordProtocols.length > 0) {
|
|
120
|
+
return ordProtocols;
|
|
121
|
+
}
|
|
122
|
+
|
|
119
123
|
// 5. No explicit protocol and no CAP endpoint - fallback to OData (Rule B)
|
|
120
124
|
const path = cds.service.protocols.path4(srvDefinition);
|
|
121
125
|
return [
|
package/lib/templates.js
CHANGED
|
@@ -348,6 +348,18 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
348
348
|
resourceDefinitions = [
|
|
349
349
|
_getResourceDefinition("openapi-v3", "json", ordId, serviceName, "oas3.json", accessStrategies),
|
|
350
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
|
+
];
|
|
351
363
|
} else {
|
|
352
364
|
// OData and others have both OpenAPI and EDMX
|
|
353
365
|
resourceDefinitions = [
|
|
@@ -699,6 +711,7 @@ module.exports = {
|
|
|
699
711
|
createAPIResourceTemplate,
|
|
700
712
|
createEventResourceTemplate,
|
|
701
713
|
createMCPAPIResourceTemplate,
|
|
714
|
+
readORDExtensions,
|
|
702
715
|
_getPackageID,
|
|
703
716
|
_getEntityTypeMappings,
|
|
704
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"
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"node": ">=18 <23"
|
|
51
52
|
},
|
|
52
53
|
"overrides": {
|
|
53
|
-
"@sap/cds-compiler": "6.
|
|
54
|
+
"@sap/cds-compiler": "6.7.3"
|
|
54
55
|
},
|
|
55
56
|
"cds": {
|
|
56
57
|
"requires": {
|