@cap-js/ord 1.4.2 → 1.4.4

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 CHANGED
@@ -114,34 +114,64 @@ This will output something like `admin:$2y$05$...` - use only the hash part (sta
114
114
 
115
115
  #### CF mTLS Authentication
116
116
 
117
- Configure Cloud Foundry mutual TLS authentication in `.cdsrc.json`:
117
+ Configure Cloud Foundry mutual TLS authentication for SAP BTP Cloud Foundry environments.
118
+
119
+ **Production Configuration with UCL (Recommended)**
120
+
121
+ For SAP UCL (Unified Customer Landscape) integration, enable mTLS in `.cdsrc.json` and configure UCL endpoints via environment variable:
118
122
 
119
123
  ```json
120
124
  {
121
- "cds": {
122
- "ord": {
123
- "authentication": {
124
- "cfMtls": {
125
- "certs": [
126
- {
127
- "issuer": "CN=SAP PKI Certificate Service Client CA,OU=SAP BTP Clients,O=SAP SE,C=DE",
128
- "subject": "CN=my-service,OU=SAP Cloud Platform Clients,O=SAP SE,C=DE"
129
- }
130
- ],
131
- "rootCaDn": ["CN=SAP Cloud Root CA,O=SAP SE,C=DE"]
132
- }
133
- }
125
+ "ord": {
126
+ "authentication": {
127
+ "cfMtls": true
134
128
  }
135
129
  }
136
130
  }
137
131
  ```
138
132
 
139
- Or use the environment variable:
133
+ ```bash
134
+ export CF_MTLS_TRUSTED_CERTS='{
135
+ "configEndpoints": ["https://your-ucl-endpoint/v1/info"],
136
+ "rootCaDn": ["CN=SAP Cloud Root CA,O=SAP SE,L=Walldorf,C=DE"]
137
+ }'
138
+ ```
139
+
140
+ **Production Configuration with Custom Certificates**
141
+
142
+ For custom certificates without UCL:
140
143
 
141
144
  ```bash
142
- CF_MTLS_TRUSTED_CERTS='{"certs":[...],"rootCaDn":[...]}'
145
+ export CF_MTLS_TRUSTED_CERTS='{
146
+ "certs": [{"issuer": "CN=My CA,O=MyOrg", "subject": "CN=my-service,O=MyOrg"}],
147
+ "rootCaDn": ["CN=My Root CA,O=MyOrg"]
148
+ }'
143
149
  ```
144
150
 
151
+ **Development Configuration**
152
+
153
+ For local development, configure the full mTLS settings directly in `.cdsrc.json`:
154
+
155
+ ```json
156
+ {
157
+ "ord": {
158
+ "authentication": {
159
+ "cfMtls": {
160
+ "certs": [
161
+ {
162
+ "issuer": "CN=Test CA,O=MyOrg,C=DE",
163
+ "subject": "CN=test-client,O=MyOrg,C=DE"
164
+ }
165
+ ],
166
+ "rootCaDn": ["CN=Test Root CA,O=MyOrg,C=DE"]
167
+ }
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ > **Note:** For detailed CF mTLS configuration options, see the [documentation](./docs/ord.md#cf-mtls-authentication).
174
+
145
175
  #### Multiple Authentication Strategies
146
176
 
147
177
  You can configure multiple authentication methods simultaneously to support different client types. Authentication types are detected automatically based on configuration presence:
@@ -179,26 +179,51 @@ async function createCfMtlsConfig(cds, Logger) {
179
179
  Logger.error("CF mTLS requires CF_INSTANCE_GUID environment variable");
180
180
  }
181
181
 
182
- // Parse configuration from single JSON environment variable or CDS settings
182
+ // Configuration priority:
183
+ // 1. cfMtls: true - Production mode, requires CF_MTLS_TRUSTED_CERTS env var
184
+ // 2. cfMtls: { ... } - Development mode, uses inline config from .cdsrc.json
185
+ // Note: Environment variable alone is NOT sufficient, explicit .cdsrc.json declaration required
186
+
183
187
  let config;
188
+ const cdsConfig = cds.env.ord?.authentication?.cfMtls;
189
+
190
+ // cfMtls must be explicitly configured in .cdsrc.json
191
+ if (!cdsConfig) {
192
+ Logger.error("CF mTLS configuration required. Set ord.authentication.cfMtls in .cdsrc.json");
193
+ return {
194
+ error: "CF mTLS configuration required. Set ord.authentication.cfMtls in .cdsrc.json",
195
+ };
196
+ }
184
197
 
185
- if (process.env.CF_MTLS_TRUSTED_CERTS) {
198
+ // Production: cfMtls: true → requires CF_MTLS_TRUSTED_CERTS environment variable
199
+ if (cdsConfig === true) {
200
+ if (!process.env.CF_MTLS_TRUSTED_CERTS) {
201
+ Logger.error(
202
+ "CF mTLS enabled with cfMtls: true but CF_MTLS_TRUSTED_CERTS environment variable is not set. " +
203
+ "Set CF_MTLS_TRUSTED_CERTS with JSON: {certs: [...], rootCaDn: [...]} or {configEndpoints: [...], rootCaDn: [...]}",
204
+ );
205
+ return {
206
+ error: "CF_MTLS_TRUSTED_CERTS environment variable required when cfMtls is set to true",
207
+ };
208
+ }
186
209
  try {
187
210
  config = JSON.parse(process.env.CF_MTLS_TRUSTED_CERTS);
188
211
  } catch {
189
- Logger.error("Failed to parse CF_MTLS_TRUSTED_CERTS");
212
+ Logger.error("Failed to parse CF_MTLS_TRUSTED_CERTS environment variable");
190
213
  return {
191
214
  error: "Invalid CF_MTLS_TRUSTED_CERTS format. Expected JSON: {certs: [...], rootCaDn: [...], configEndpoints: [...]}",
192
215
  };
193
216
  }
194
- } else {
195
- config = cds.env.ord?.authentication?.cfMtls;
196
217
  }
197
-
198
- if (!config) {
199
- Logger.error("CF mTLS configuration required. Set CF_MTLS_TRUSTED_CERTS or cds.env.ord.authentication.cfMtls");
218
+ // Development: cfMtls: { ... } → use inline config from .cdsrc.json
219
+ else if (typeof cdsConfig === "object") {
220
+ config = cdsConfig;
221
+ }
222
+ // Invalid configuration type
223
+ else {
224
+ Logger.error("Invalid cfMtls configuration. Expected true or object with certs/rootCaDn/configEndpoints");
200
225
  return {
201
- error: "CF mTLS configuration required",
226
+ error: "Invalid cfMtls configuration. Expected true or object",
202
227
  };
203
228
  }
204
229
 
@@ -268,7 +293,7 @@ async function createCfMtlsConfig(cds, Logger) {
268
293
  if (certs.length === 0) {
269
294
  Logger.error(
270
295
  "CF mTLS requires at least one certificate pair. " +
271
- "Provide via certs array or configEndpoints in CF_MTLS_TRUSTED_CERTS or cds.env.ord.cfMtls",
296
+ "Provide via certs array or configEndpoints in CF_MTLS_TRUSTED_CERTS or ord.authentication.cfMtls",
272
297
  );
273
298
  return {
274
299
  error: "CF mTLS requires at least one certificate pair",
@@ -278,7 +303,7 @@ async function createCfMtlsConfig(cds, Logger) {
278
303
  if (rootCaDn.length === 0) {
279
304
  Logger.error(
280
305
  "CF mTLS requires at least one root CA. " +
281
- "Provide via rootCaDn array in CF_MTLS_TRUSTED_CERTS or cds.env.ord.cfMtls",
306
+ "Provide via rootCaDn array in CF_MTLS_TRUSTED_CERTS or ord.authentication.cfMtls",
282
307
  );
283
308
  return {
284
309
  error: "CF mTLS requires at least one root CA",
package/lib/build.js CHANGED
@@ -6,6 +6,8 @@ const cliProgress = require("cli-progress");
6
6
  const { BUILD_DEFAULT_PATH, ORD_SERVICE_NAME, ORD_DOCUMENT_FILE_NAME } = require("./constants");
7
7
  const { isMCPPluginInPackageJson } = require("./mcpAdapter");
8
8
 
9
+ const { BuildError } = cds.build;
10
+
9
11
  module.exports = class OrdBuildPlugin extends cds.build.Plugin {
10
12
  static taskDefaults = { src: cds.env.folders.srv };
11
13
 
@@ -15,54 +17,46 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
15
17
  }
16
18
  }
17
19
 
18
- async _writeResourcesFiles(resObj, model, promises) {
19
- let totalFiles = resObj.reduce((total, resource) => {
20
- if (!resource.ordId.includes(ORD_SERVICE_NAME) && resource.resourceDefinitions) {
21
- return total + resource.resourceDefinitions.length;
22
- }
23
- return total;
24
- }, 0);
25
-
20
+ _createProgressBar(totalFiles) {
26
21
  const progressBar = new cliProgress.SingleBar({
27
22
  format: "Processing resourcesFiles [{bar}] {percentage}% | {value}/{total} | ETA: {eta}s",
28
23
  barCompleteChar: "█",
29
24
  barIncompleteChar: "░",
30
25
  stopOnComplete: true,
31
26
  });
27
+ progressBar.start(totalFiles, 0);
28
+ return progressBar;
29
+ }
30
+
31
+ _countTotalFiles(resObj) {
32
+ return resObj.reduce((total, resource) => {
33
+ if (!resource.ordId.includes(ORD_SERVICE_NAME) && resource.resourceDefinitions) {
34
+ return total + resource.resourceDefinitions.length;
35
+ }
36
+ return total;
37
+ }, 0);
38
+ }
32
39
 
33
- let warnings = [];
40
+ async _writeResourcesFiles(resObj, model, promises) {
41
+ const totalFiles = this._countTotalFiles(resObj);
42
+ const progressBar = this._createProgressBar(totalFiles);
34
43
  let completed = 0;
35
- progressBar.start(totalFiles, 0);
36
44
 
37
45
  try {
38
46
  for (const resource of resObj) {
39
- // Generate if has service definitions OR has MCP plugin
40
47
  const shouldGenerate = resource.resourceDefinitions || isMCPPluginInPackageJson();
41
48
  if (!shouldGenerate) continue;
49
+
42
50
  for (const resourceDefinition of resource.resourceDefinitions) {
43
- try {
44
- const { _, response } = await getMetadata(resourceDefinition.url, model); // eslint-disable-line no-unused-vars
45
- const fileName = path
46
- .join(resource.ordId, resourceDefinition.url.split("/").pop())
47
- .replace(/:/g, "_");
48
- promises.push(
49
- this.write(response)
50
- .to(fileName)
51
- .catch((err) => {
52
- warnings.push(`Error writing file ${fileName}: ${err.message}`);
53
- }),
54
- );
55
- } catch (error) {
56
- warnings.push(`Error getting metadata for ${resourceDefinition.url}: ${error.message}`);
57
- }
58
- completed++;
59
- progressBar.update(completed);
51
+ const { _, response } = await getMetadata(resourceDefinition.url, model); // eslint-disable-line no-unused-vars
52
+ const fileName = path
53
+ .join(resource.ordId, resourceDefinition.url.split("/").pop())
54
+ .replace(/:/g, "_");
55
+ promises.push(this.write(response).to(fileName));
56
+ progressBar.update(++completed);
60
57
  }
61
58
  }
62
59
  await Promise.all(promises);
63
- } catch (error) {
64
- warnings.push("Failed to process resources: " + error.message);
65
- throw error;
66
60
  } finally {
67
61
  progressBar.stop();
68
62
  }
@@ -74,9 +68,7 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
74
68
  for (const resource of resources || []) {
75
69
  if (resource.resourceDefinitions) {
76
70
  for (const resourceDefinition of resource.resourceDefinitions) {
77
- let url = resourceDefinition.url;
78
- url = this._createRelativePath(url);
79
- resourceDefinition.url = url;
71
+ resourceDefinition.url = this._createRelativePath(resourceDefinition.url);
80
72
  }
81
73
  }
82
74
  }
@@ -91,25 +83,29 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
91
83
  _createRelativePath(url) {
92
84
  let relative = url.split("/ord/v1").pop();
93
85
  if (relative.startsWith("/")) relative = relative.slice(1);
94
- relative = relative.replace(/:/g, "_");
95
- return path.join(...relative.split("/"));
86
+ return path.join(...relative.replace(/:/g, "_").split("/"));
96
87
  }
97
88
 
98
89
  async build() {
99
- const model = await this.model();
100
- const ordDocument = ord(model);
101
- const postProcessedOrdDocument = this.postProcess(ordDocument);
90
+ try {
91
+ const model = await this.model();
92
+ const ordDocument = ord(model);
93
+ const postProcessedOrdDocument = this.postProcess(ordDocument);
102
94
 
103
- const promises = [];
104
- promises.push(this.write(postProcessedOrdDocument).to(ORD_DOCUMENT_FILE_NAME));
95
+ const promises = [];
96
+ promises.push(this.write(postProcessedOrdDocument).to(ORD_DOCUMENT_FILE_NAME));
105
97
 
106
- if (ordDocument.apiResources && ordDocument.apiResources.length > 0) {
107
- await this._writeResourcesFiles(ordDocument.apiResources, model, promises);
108
- }
98
+ if (ordDocument.apiResources?.length > 0) {
99
+ await this._writeResourcesFiles(ordDocument.apiResources, model, promises);
100
+ }
109
101
 
110
- if (ordDocument.eventResources && ordDocument.eventResources.length > 0) {
111
- await this._writeResourcesFiles(ordDocument.eventResources, model, promises);
102
+ if (ordDocument.eventResources?.length > 0) {
103
+ await this._writeResourcesFiles(ordDocument.eventResources, model, promises);
104
+ }
105
+
106
+ return Promise.all(promises);
107
+ } catch (error) {
108
+ throw new BuildError(`ORD build failed: ${error.message}`);
112
109
  }
113
- return Promise.all(promises);
114
110
  }
115
111
  };
package/lib/constants.js CHANGED
@@ -77,6 +77,8 @@ const LEVEL = Object.freeze({
77
77
 
78
78
  const OPEN_RESOURCE_DISCOVERY_VERSION = "1.12";
79
79
 
80
+ const OPENAPI_SERVERS_ANNOTATION = "@OpenAPI.servers";
81
+
80
82
  const ORD_EXTENSIONS_PREFIX = "@ORD.Extensions.";
81
83
 
82
84
  const ORD_ODM_ENTITY_NAME_ANNOTATION = "@ODM.entityName";
@@ -115,6 +117,39 @@ const SEM_VERSION_REGEX =
115
117
 
116
118
  const MCP_CUSTOM_TYPE = "sap:mcp-server-card:v0";
117
119
 
120
+ // ORD apiProtocol values
121
+ const ORD_API_PROTOCOL = Object.freeze({
122
+ ODATA_V4: "odata-v4",
123
+ ODATA_V2: "odata-v2",
124
+ REST: "rest",
125
+ GRAPHQL: "graphql",
126
+ SAP_INA: "sap-ina-api-v1",
127
+ SAP_DATA_SUBSCRIPTION: "sap.dp:data-subscription-api:v1",
128
+ });
129
+
130
+ // Mapping from CAP protocol kind to ORD apiProtocol
131
+ // CAP may return 'odata', 'odata-v4', 'rest', etc.
132
+ const CAP_TO_ORD_PROTOCOL_MAP = Object.freeze({
133
+ "odata": ORD_API_PROTOCOL.ODATA_V4,
134
+ "odata-v4": ORD_API_PROTOCOL.ODATA_V4,
135
+ "odata-v2": ORD_API_PROTOCOL.ODATA_V2,
136
+ "rest": ORD_API_PROTOCOL.REST,
137
+ });
138
+
139
+ // Protocols that ORD supports but CAP doesn't recognize (endpoints4 returns [])
140
+ // These need special handling in the ORD plugin
141
+ const ORD_ONLY_PROTOCOLS = Object.freeze({
142
+ "ina": {
143
+ apiProtocol: ORD_API_PROTOCOL.SAP_INA,
144
+ hasEntryPoints: false,
145
+ hasResourceDefinitions: false,
146
+ },
147
+ });
148
+
149
+ // Protocols that the ORD plugin cannot currently generate definitions for
150
+ // GraphQL is supported by ORD spec, but plugin can't emit graphql-sdl yet
151
+ const PLUGIN_UNSUPPORTED_PROTOCOLS = Object.freeze(["graphql"]);
152
+
118
153
  // CF mTLS Error Reasons
119
154
  const CF_MTLS_ERROR_REASON = Object.freeze({
120
155
  NO_HEADERS: "NO_HEADERS",
@@ -144,6 +179,7 @@ module.exports = {
144
179
  BASIC_AUTH_HEADER_KEY,
145
180
  BUILD_DEFAULT_PATH,
146
181
  BLOCKED_SERVICE_NAME,
182
+ CAP_TO_ORD_PROTOCOL_MAP,
147
183
  CDS_ELEMENT_KIND,
148
184
  CF_MTLS_HEADERS,
149
185
  COMPILER_TYPES,
@@ -156,13 +192,17 @@ module.exports = {
156
192
  LEVEL,
157
193
  MCP_CUSTOM_TYPE,
158
194
  OPEN_RESOURCE_DISCOVERY_VERSION,
195
+ OPENAPI_SERVERS_ANNOTATION,
159
196
  ORD_ACCESS_STRATEGY,
197
+ ORD_API_PROTOCOL,
160
198
  ORD_DOCUMENT_FILE_NAME,
161
199
  ORD_EXTENSIONS_PREFIX,
162
200
  ORD_ODM_ENTITY_NAME_ANNOTATION,
163
201
  ORD_EXISTING_PRODUCT_PROPERTY,
202
+ ORD_ONLY_PROTOCOLS,
164
203
  ORD_RESOURCE_TYPE,
165
204
  ORD_SERVICE_NAME,
205
+ PLUGIN_UNSUPPORTED_PROTOCOLS,
166
206
  RESOURCE_VISIBILITY,
167
207
  ALLOWED_VISIBILITY,
168
208
  IMPLEMENTATIONSTANDARD_VERSIONS,
package/lib/metaData.js CHANGED
@@ -1,12 +1,24 @@
1
1
  const cds = require("@sap/cds/lib");
2
2
  const { compile: openapi } = require("@cap-js/openapi");
3
3
  const { compile: asyncapi } = require("@cap-js/asyncapi");
4
- const { COMPILER_TYPES } = require("./constants");
4
+ const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");
5
5
  const Logger = require("./logger");
6
6
  const { interopCSN } = require("./interopCsn.js");
7
7
  const cdsc = require("@sap/cds-compiler/lib/main");
8
8
  const { isMCPPluginReady, buildMcpServerDefinition } = require("./mcpAdapter");
9
9
 
10
+ /**
11
+ * Read @OpenAPI.servers annotation from service definition
12
+ * @param {object} csn - The CSN model
13
+ * @param {string} serviceName - The service name
14
+ * @returns {string|undefined} - JSON string of servers array or undefined
15
+ */
16
+ const _getServersFromAnnotation = (csn, serviceName) => {
17
+ const servers = csn?.definitions?.[serviceName]?.[OPENAPI_SERVERS_ANNOTATION];
18
+ const isValidServers = Array.isArray(servers) && servers.length > 0;
19
+ return isValidServers ? JSON.stringify(servers) : undefined;
20
+ };
21
+
10
22
  const getMetadata = async (url, model = null) => {
11
23
  const parts = url
12
24
  ?.split("/")
@@ -23,7 +35,16 @@ const getMetadata = async (url, model = null) => {
23
35
  switch (compilerType) {
24
36
  case COMPILER_TYPES.oas3:
25
37
  try {
26
- responseFile = openapi(csn, { ...options, ...(compileOptions?.openapi || {}) });
38
+ // Check for service-level @OpenAPI.servers annotation
39
+ const serversFromAnnotation = _getServersFromAnnotation(csn, serviceName);
40
+ const openapiOptions = { ...options, ...(compileOptions?.openapi || {}) };
41
+
42
+ // Service-level annotation takes precedence over global config
43
+ if (serversFromAnnotation) {
44
+ openapiOptions["openapi:servers"] = serversFromAnnotation;
45
+ }
46
+
47
+ responseFile = openapi(csn, openapiOptions);
27
48
  } catch (error) {
28
49
  Logger.error("OpenApi error:", error.message);
29
50
  throw error;
@@ -0,0 +1,134 @@
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|null} Protocol name, or null if not explicitly set.
27
+ */
28
+ function _getExplicitProtocol(srvDefinition) {
29
+ const protocol = srvDefinition["@protocol"];
30
+ if (!protocol) {
31
+ return null;
32
+ }
33
+ return Array.isArray(protocol) ? protocol[0] : 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 fallback 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 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
+ const capEndpoints = _getCapEndpoints(serviceName, srvDefinition);
93
+ for (const endpoint of capEndpoints) {
94
+ if (PLUGIN_UNSUPPORTED_PROTOCOLS.includes(endpoint.kind)) {
95
+ Logger.warn(
96
+ `Protocol '${endpoint.kind}' is supported by ORD but this plugin cannot generate its resource definitions yet.`,
97
+ );
98
+ continue;
99
+ }
100
+
101
+ const apiProtocol = CAP_TO_ORD_PROTOCOL_MAP[endpoint.kind] ?? endpoint.kind;
102
+ if (apiProtocol) {
103
+ return [
104
+ {
105
+ apiProtocol,
106
+ entryPoints: endpoint.path ? [endpoint.path] : [],
107
+ hasResourceDefinitions: true,
108
+ },
109
+ ];
110
+ }
111
+ }
112
+
113
+ // 4. Handle explicit protocol with no CAP endpoint (Rule A)
114
+ if (explicit) {
115
+ Logger.warn(`Unknown protocol '${explicit}' is not supported, skipping service '${serviceName}'.`);
116
+ return [];
117
+ }
118
+
119
+ // 5. No explicit protocol and no CAP endpoint - fallback to OData (Rule B)
120
+ const path = cds.service.protocols.path4(srvDefinition);
121
+ return [
122
+ {
123
+ apiProtocol: ORD_API_PROTOCOL.ODATA_V4,
124
+ entryPoints: path ? [path] : [],
125
+ hasResourceDefinitions: true,
126
+ },
127
+ ];
128
+ }
129
+
130
+ module.exports = {
131
+ resolveApiResourceProtocol,
132
+ // Exported for testing
133
+ _getExplicitProtocol,
134
+ };
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,36 @@ 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 {
352
+ // OData and others have both OpenAPI and EDMX
353
+ resourceDefinitions = [
354
+ _getResourceDefinition("openapi-v3", "json", ordId, serviceName, "oas3.json", accessStrategies),
355
+ _getResourceDefinition("edmx", "xml", ordId, serviceName, "edmx", accessStrategies),
356
+ ];
357
+ }
365
358
  }
366
359
 
367
360
  const entityTypeMappings = _getEntityTypeMappings(serviceDefinition);
@@ -378,9 +371,9 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
378
371
  partOfPackage: packageId,
379
372
  partOfGroups: [_getGroupID(serviceDefinition, defaults.groupTypeId, appConfig)],
380
373
  releaseStatus: "active",
381
- apiProtocol: generatedPath.kind === "odata" ? "odata-v4" : generatedPath.kind,
382
- resourceDefinitions: resourceDefinitions,
383
- entryPoints: [generatedPath.path],
374
+ apiProtocol,
375
+ resourceDefinitions,
376
+ entryPoints,
384
377
  extensible: {
385
378
  supported: "no",
386
379
  },
@@ -389,24 +382,13 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
389
382
  ...ordExtensions,
390
383
  };
391
384
 
385
+ // Special handling for data product services
392
386
  if (isPrimaryDataProductService(serviceDefinition)) {
393
- obj.apiProtocol = "sap.dp:data-subscription-api:v1";
394
387
  obj.direction = "outbound";
395
- obj.entryPoints = [];
396
388
  if (extracted) {
397
389
  // Overwrite partOfGroups
398
390
  obj.partOfGroups = [`${defaults.groupTypeId}:${appConfig.ordNamespace}:${cleanServiceName}`];
399
391
  }
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
392
  }
411
393
 
412
394
  if (obj.visibility !== RESOURCE_VISIBILITY.private) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/ord",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "CAP Plugin for generating ORD document.",
5
5
  "repository": "cap-js/ord",
6
6
  "author": "SAP SE (https://www.sap.com)",
@@ -23,6 +23,7 @@
23
23
  "test": "jest __tests__/unit --ci --collectCoverage",
24
24
  "test:integration:basic": "jest __tests__/integration/basic-auth.test.js --testPathIgnorePatterns=/node_modules/ --forceExit",
25
25
  "test:integration:mtls": "jest __tests__/integration/mtls-auth.test.js --testPathIgnorePatterns=/node_modules/ --forceExit",
26
+ "test:integration:build": "jest __tests__/integration/cds-build.test.js --testPathIgnorePatterns=/node_modules/ --forceExit",
26
27
  "update-snapshot": "jest --ci --updateSnapshot",
27
28
  "cds:version": "cds v -i"
28
29
  },
@@ -30,7 +31,7 @@
30
31
  "eslint": "^9.2.0",
31
32
  "express": "^4",
32
33
  "jest": "^30.0.0",
33
- "prettier": "3.7.4",
34
+ "prettier": "3.8.1",
34
35
  "supertest": "^7.0.0",
35
36
  "@cap-js/sqlite": "^2",
36
37
  "@sap/cds-dk": ">=8.9.5"
@@ -48,6 +49,9 @@
48
49
  "engines": {
49
50
  "node": ">=18 <23"
50
51
  },
52
+ "overrides": {
53
+ "@sap/cds-compiler": "6.6.2"
54
+ },
51
55
  "cds": {
52
56
  "requires": {
53
57
  "SAP ORD Service": {