@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 +46 -16
- package/lib/auth/cf-mtls.js +36 -11
- package/lib/build.js +43 -47
- package/lib/constants.js +40 -0
- package/lib/metaData.js +23 -2
- package/lib/protocol-resolver.js +134 -0
- package/lib/templates.js +45 -63
- package/package.json +6 -2
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
|
|
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
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
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
|
-
|
|
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='{
|
|
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:
|
package/lib/auth/cf-mtls.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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 (
|
|
199
|
-
|
|
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: "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
return path.join(...relative.split("/"));
|
|
86
|
+
return path.join(...relative.replace(/:/g, "_").split("/"));
|
|
96
87
|
}
|
|
97
88
|
|
|
98
89
|
async build() {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
try {
|
|
91
|
+
const model = await this.model();
|
|
92
|
+
const ordDocument = ord(model);
|
|
93
|
+
const postProcessedOrdDocument = this.postProcess(ordDocument);
|
|
102
94
|
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
const promises = [];
|
|
96
|
+
promises.push(this.write(postProcessedOrdDocument).to(ORD_DOCUMENT_FILE_NAME));
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
98
|
+
if (ordDocument.apiResources?.length > 0) {
|
|
99
|
+
await this._writeResourcesFiles(ordDocument.apiResources, model, promises);
|
|
100
|
+
}
|
|
109
101
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
382
|
-
resourceDefinitions
|
|
383
|
-
entryPoints
|
|
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.
|
|
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.
|
|
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": {
|