@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 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("OpenApi error:", error.message);
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("AsyncApi error:", error.message);
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("Csn error:", error.message);
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("Edmx error:", error.message);
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("MCP server definition error:", error.message);
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: `application/${compilerType === "edmx" ? "xml" : "json"}`,
111
+ contentType:
112
+ compilerType === "graphql" ? "text/plain" : `application/${compilerType === "edmx" ? "xml" : "json"}`,
98
113
  response: responseFile,
99
114
  };
100
115
  };
@@ -31,7 +31,8 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
31
31
  return res.status(404).send("404 Not Found");
32
32
  });
33
33
 
34
- cds.app.get(`/ord/v1/:ordId?/:service?`, authMiddleware, async (req, res) => {
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?.add(api.partOfPackage));
334
- ordDocument.eventResources?.forEach((event) => usedPackageIds?.add(event.partOfPackage));
335
- ordDocument.dataProducts?.forEach((dp) => usedPackageIds?.add(dp.partOfPackage));
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 && et.partOfPackage) {
339
+ if (et?.partOfPackage) {
338
340
  usedPackageIds.add(et.partOfPackage);
339
341
  }
340
342
  });
341
343
 
342
- const filteredPackages = ordDocument.packages.filter((pkg) => usedPackageIds.has(pkg.ordId));
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 paths = _generatePaths(serviceName, serviceDefinition);
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
- paths.forEach((generatedPath) => {
356
- let resourceDefinitions = [
357
- _getResourceDefinition("openapi-v3", "json", ordId, serviceName, "oas3.json", accessStrategies),
358
- ];
359
-
360
- if (generatedPath.kind !== "rest") {
361
- //edmx resource definition is not generated in case of 'rest' protocol
362
- resourceDefinitions.push(
363
- _getResourceDefinition("edmx", "xml", ordId, serviceName, "edmx", accessStrategies),
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: generatedPath.kind === "odata" ? "odata-v4" : generatedPath.kind,
382
- resourceDefinitions: resourceDefinitions,
383
- entryPoints: [generatedPath.path],
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",
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
- "eslint": "^9.2.0",
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": {