Skip to content

Commit 9809bf0

Browse files
authored
feat: support @OpenAPI.servers annotation for service-level server URLs (#339)
Add support for configuring OpenAPI servers via CDS annotation to enable SAP Business Accelerator Hub "Try Out" feature with production URLs. Closes #338
1 parent 11e95ba commit 9809bf0

File tree

7 files changed

+109
-2
lines changed

7 files changed

+109
-2
lines changed

__tests__/integration/cds-build.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ describe("ORD Build Integration Tests", () => {
107107
}
108108
}
109109
});
110+
111+
test("should include @OpenAPI.servers annotation in OpenAPI document", () => {
112+
const testServiceApi = ordDocument.apiResources.find((api) => api.ordId.includes("TestService"));
113+
expect(testServiceApi).toBeDefined();
114+
115+
const openApiDef = testServiceApi.resourceDefinitions.find((def) => def.url.endsWith(".oas3.json"));
116+
expect(openApiDef).toBeDefined();
117+
118+
const openApiPath = path.join(ORD_GEN_DIR, openApiDef.url);
119+
const openApiContent = JSON.parse(fs.readFileSync(openApiPath, "utf-8"));
120+
121+
expect(openApiContent.servers).toBeDefined();
122+
expect(openApiContent.servers).toHaveLength(2);
123+
expect(openApiContent.servers[0].url).toBe("https://test-service.api.example.com");
124+
expect(openApiContent.servers[1].url).toBe("https://test-service-sandbox.api.example.com");
125+
});
110126
});
111127

112128
describe("Failed Build", () => {

__tests__/integration/integration-test-app/srv/test-service.cds

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ annotate TestService with @ORD.Extensions: {
2020
visibility : 'public',
2121
version : '1.0.0'
2222
};
23+
24+
annotate TestService with @OpenAPI.servers: [
25+
{ url: 'https://test-service.api.example.com', description: 'Production' },
26+
{ url: 'https://test-service-sandbox.api.example.com', description: 'Sandbox' }
27+
];

__tests__/unit/metaData.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,40 @@ describe("metaData", () => {
242242
expect(result.contentType).toBe("application/json");
243243
expect(result.response).toBe("Content with compile options");
244244
});
245+
246+
describe("@OpenAPI.servers annotation", () => {
247+
const url = "/ord/v1/ns:apiResource:TestService:v1/TestService.oas3.json";
248+
let capturedOptions;
249+
250+
beforeEach(() => {
251+
capturedOptions = null;
252+
openapi.mockImplementation((csn, options) => {
253+
capturedOptions = options;
254+
return "content";
255+
});
256+
});
257+
258+
test("should pass servers from annotation to openapi compiler", async () => {
259+
const servers = [{ url: "https://api.example.com", description: "Production" }];
260+
const mockCsn = {
261+
definitions: { TestService: { "@OpenAPI.servers": servers } },
262+
};
263+
264+
await getMetadata(url, mockCsn);
265+
266+
expect(capturedOptions["openapi:servers"]).toBe(JSON.stringify(servers));
267+
});
268+
269+
test.each([
270+
["missing", {}],
271+
["empty array", { "@OpenAPI.servers": [] }],
272+
["not an array", { "@OpenAPI.servers": "invalid" }],
273+
])("should not set servers when annotation is %s", async (_, serviceDef) => {
274+
const mockCsn = { definitions: { TestService: serviceDef } };
275+
276+
await getMetadata(url, mockCsn);
277+
278+
expect(capturedOptions["openapi:servers"]).toBeUndefined();
279+
});
280+
});
245281
});

docs/ord.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
2. [Configuration](#configuration)
77
- [Global Application Settings](#1-overriding-global-application-information)
88
- [Service-Level Customization](#2-overriding-service-level-information)
9+
- [OpenAPI Servers](#3-openapi-servers-configuration)
910
3. [Custom ORD Content](#adding-custom-ord-content)
1011
4. [Products](#adding-products)
1112
- [Using Existing SAP Products](#1-using-an-existing-sap-product)
@@ -66,6 +67,18 @@ annotate ProcessorService with @ORD.Extensions: {
6667
6768
---
6869

70+
### 3. OpenAPI Servers Configuration
71+
72+
Use `@OpenAPI.servers` to add production URLs to generated OpenAPI documents (for SAP Business Accelerator Hub "Try Out" feature):
73+
74+
```js
75+
annotate MyService with @OpenAPI.servers: [
76+
{ url: 'https://my-service.api.sap.com', description: 'Production' }
77+
];
78+
```
79+
80+
---
81+
6982
## Adding Custom ORD Content
7083

7184
The ORD plugin allows adding **custom ORD content** via the `customOrdContentFile` setting in `.cdsrc.json`.
@@ -369,6 +382,7 @@ More information, see [ORD Document specification](https://pages.github.tools.sa
369382
| -------------------------------- | ----------------------------------------------------------------------- |
370383
| Global Metadata | Define in `.cdsrc.json` under `ord` |
371384
| Service Metadata | Use `@ORD.Extensions` annotations in `.cds` files |
385+
| OpenAPI Servers | Use `@OpenAPI.servers` annotation |
372386
| Custom ORD Content | Use `customOrdContentFile` |
373387
| Linking to Existing SAP Products | Use `existingProductORDId` |
374388
| Defining Custom Products | Add `products` section manually |

lib/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ const LEVEL = Object.freeze({
7777

7878
const OPEN_RESOURCE_DISCOVERY_VERSION = "1.12";
7979

80+
const OPENAPI_SERVERS_ANNOTATION = "@OpenAPI.servers";
81+
8082
const ORD_EXTENSIONS_PREFIX = "@ORD.Extensions.";
8183

8284
const ORD_ODM_ENTITY_NAME_ANNOTATION = "@ODM.entityName";
@@ -156,6 +158,7 @@ module.exports = {
156158
LEVEL,
157159
MCP_CUSTOM_TYPE,
158160
OPEN_RESOURCE_DISCOVERY_VERSION,
161+
OPENAPI_SERVERS_ANNOTATION,
159162
ORD_ACCESS_STRATEGY,
160163
ORD_DOCUMENT_FILE_NAME,
161164
ORD_EXTENSIONS_PREFIX,

lib/metaData.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
const cds = require("@sap/cds/lib");
22
const { compile: openapi } = require("@cap-js/openapi");
33
const { compile: asyncapi } = require("@cap-js/asyncapi");
4-
const { COMPILER_TYPES } = require("./constants");
4+
const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");
55
const Logger = require("./logger");
66
const { interopCSN } = require("./interopCsn.js");
77
const cdsc = require("@sap/cds-compiler/lib/main");
88
const { isMCPPluginReady, buildMcpServerDefinition } = require("./mcpAdapter");
99

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+
1022
const getMetadata = async (url, model = null) => {
1123
const parts = url
1224
?.split("/")
@@ -23,7 +35,16 @@ const getMetadata = async (url, model = null) => {
2335
switch (compilerType) {
2436
case COMPILER_TYPES.oas3:
2537
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);
2748
} catch (error) {
2849
Logger.error("OpenApi error:", error.message);
2950
throw error;

xmpl/srv/services.cds

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ service LocalService {
3636

3737
annotate LocalService with @ORD.Extensions: {title: 'This is Local Service title'};
3838

39+
// Service-level OpenAPI servers configuration
40+
annotate LocalService with @OpenAPI.servers: [
41+
{
42+
url : 'https://local-service.api.sap.com',
43+
description: 'LocalService Production'
44+
},
45+
{
46+
url : 'https://local-service-sandbox.api.sap.com',
47+
description: 'LocalService Sandbox'
48+
}
49+
];
50+
3951
annotate AdminService with @ORD.Extensions: {
4052
title : 'This is Admin Service title',
4153
industry : [

0 commit comments

Comments
 (0)