@cap-js/ord 1.5.0 → 1.7.0

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/cds-plugin.js CHANGED
@@ -4,17 +4,7 @@ if (cds.cli.command === "build") {
4
4
  cds.build?.register?.("ord", require("./lib/build"));
5
5
  }
6
6
 
7
- function _lazyRegisterCompileTarget() {
8
- const ord = require("./lib/index").ord;
9
- Object.defineProperty(this, "ord", { ord });
10
- return ord;
11
- }
12
-
13
- const registerORDCompileTarget = () => {
14
- Object.defineProperty(cds.compile.to, "ord", {
15
- get: _lazyRegisterCompileTarget,
16
- configurable: true,
17
- });
18
- };
19
-
20
- registerORDCompileTarget();
7
+ Object.defineProperty(cds.compile.to, "ord", {
8
+ configurable: true,
9
+ get: () => (model) => require("./lib/index").ord(model, []),
10
+ });
@@ -5,7 +5,7 @@
5
5
  * from external configuration endpoints and merge with static configuration.
6
6
  */
7
7
 
8
- /* global AbortController */
8
+ /* global AbortController */ // eslint-disable-line no-redeclare
9
9
 
10
10
  const { HTTP_CONFIG } = require("../constants");
11
11
 
package/lib/build.js CHANGED
@@ -1,12 +1,14 @@
1
+ const os = require("os");
1
2
  const path = require("path");
2
3
  const cds = require("@sap/cds");
3
4
  const _ = require("lodash");
4
5
  const Piscina = require("piscina");
5
6
  const cliProgress = require("cli-progress");
6
7
 
7
- const os = require("os");
8
+ const defaults = require("./defaults.js");
8
9
  const { ord } = require("./index");
9
- const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME } = require("./constants");
10
+ const registerCompileTargets = require("./common/register-compile-targets");
11
+ const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME, DOCUMENT_PERSPECTIVES } = require("./constants");
10
12
 
11
13
  module.exports = class OrdBuildPlugin extends cds.build.Plugin {
12
14
  static taskDefaults = { src: cds.env.folders.srv };
@@ -16,13 +18,7 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
16
18
  }
17
19
 
18
20
  async build() {
19
- // @cap-js/graphql registers protocols and compile targets at runtime via cds-plugin.js,
20
- // but cds build rebuilds cds.env afterward, losing both registrations.
21
- // Re-apply so endpoints4() and cds.compile.to.graphql work during build.
22
- if ("@cap-js/graphql" in cds.env.plugins && !cds.env.protocols?.graphql) {
23
- cds.env.protocols.graphql = { path: "/graphql", impl: "@cap-js/graphql" };
24
- require("@cap-js/graphql/lib/api").registerCompileTargets();
25
- }
21
+ registerCompileTargets();
26
22
 
27
23
  return this.model()
28
24
  .then((model) => ({ model, document: ord(model) }))
@@ -58,7 +54,7 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
58
54
  resourceDefinition.url = this._createRelativePath(resourceDefinition.url);
59
55
  });
60
56
 
61
- return clone;
57
+ return defaults.adjustForPerspective(clone, DOCUMENT_PERSPECTIVES.SystemVersion);
62
58
  }
63
59
 
64
60
  _createRelativePath(url) {
@@ -0,0 +1,31 @@
1
+ const cds = require("@sap/cds");
2
+ const Logger = require("../logger");
3
+
4
+ const PROTOCOL_PROVIDERS = Object.freeze({
5
+ ["@cap-js/mcp"]: "mcp",
6
+ ["@cap-js/graphql"]: "graphql",
7
+ });
8
+
9
+ module.exports = () => {
10
+ // Plugins like @cap-js/mcp and @cap-js/graphql register protocols and compile targets
11
+ // at runtime via cds-plugin.js, but cds build rebuilds cds.env afterward, losing both
12
+ // registrations. Re-apply so that compilation works accordingly during build and runtime.
13
+
14
+ Object.keys(cds.env.plugins || {})
15
+ .filter((plugin) => !!PROTOCOL_PROVIDERS[plugin])
16
+ .forEach((plugin) => {
17
+ try {
18
+ const protocol = PROTOCOL_PROVIDERS[plugin];
19
+
20
+ require(`${plugin}/lib/api`)?.registerCompileTargets?.();
21
+ cds.env.protocols = {
22
+ ...cds.env.protocols,
23
+ ...(!protocol || cds.env.protocols?.[protocol]
24
+ ? {}
25
+ : { [protocol]: { path: `/${protocol}`, impl: plugin } }),
26
+ };
27
+ } catch (error) {
28
+ Logger.warn(`Failed to register compile targets for ${plugin}: ${error}`);
29
+ }
30
+ });
31
+ };
@@ -0,0 +1,161 @@
1
+ const path = require("path");
2
+ const cds = require("@sap/cds");
3
+ const _ = require("lodash");
4
+ const { readFileSync } = require("fs");
5
+
6
+ const Logger = require("./logger");
7
+ const { getRFC3339Date } = require("./date");
8
+ const defaults = require("./defaults");
9
+ const { createEntityTypeMappingsItemTemplate } = require("./templates");
10
+ const { CONTENT_MERGE_KEY, CDS_ELEMENT_KIND } = require("./constants");
11
+
12
+ module.exports = class Configuration {
13
+ constructor(csn) {
14
+ const env = cds.env["ord"];
15
+ const packageName = this._loadNameFromPackageJson();
16
+ const eventApplicationNamespace = cds.env?.export?.asyncapi?.applicationNamespace;
17
+ const ordNamespace = cds.env["ord"]?.namespace || `customer.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
18
+ const internalNamespace = env?.internalNamespace;
19
+
20
+ if (eventApplicationNamespace && ordNamespace !== eventApplicationNamespace) {
21
+ Logger.warn("ORD and AsyncAPI namespaces should be the same.");
22
+ }
23
+
24
+ this._env = env;
25
+ this._packageName = packageName;
26
+ this._ordNamespace = ordNamespace;
27
+ this._internalNamespace = internalNamespace;
28
+ this._lastUpdate = getRFC3339Date();
29
+ this._serviceNames = this._resolveServiceNames(csn);
30
+ this._existingProductORDId = env?.existingProductORDId;
31
+ this._apiResourceNames = this._resolveApiResourceNames(csn);
32
+ this._entityTypeTargets = this._resolveEntityTypeTargets(csn);
33
+ this._eventServiceNames = this._resolveEventServiceNames(csn);
34
+ this._appName = packageName.replace(/^@/, "").replace(/[@/]/g, "-");
35
+ this._policyLevels = env?.policyLevels || (env?.policyLevel && [env?.policyLevel]) || defaults.policyLevels;
36
+ }
37
+
38
+ get env() {
39
+ return this._env;
40
+ }
41
+
42
+ get appName() {
43
+ return this._appName;
44
+ }
45
+
46
+ get lastUpdate() {
47
+ return this._lastUpdate;
48
+ }
49
+
50
+ get packageName() {
51
+ return this._packageName;
52
+ }
53
+
54
+ get serviceNames() {
55
+ return this._serviceNames;
56
+ }
57
+
58
+ get ordNamespace() {
59
+ return this._ordNamespace;
60
+ }
61
+
62
+ get internalNamespace() {
63
+ return this._internalNamespace;
64
+ }
65
+
66
+ get policyLevels() {
67
+ return this._policyLevels;
68
+ }
69
+
70
+ get apiResourceNames() {
71
+ return this._apiResourceNames;
72
+ }
73
+
74
+ get entityTypeTargets() {
75
+ return this._entityTypeTargets;
76
+ }
77
+
78
+ get eventServiceNames() {
79
+ return this._eventServiceNames;
80
+ }
81
+
82
+ get hasSAPPolicyLevel() {
83
+ return this.policyLevels.some((policyLevel) => policyLevel.split(":")[0].toLowerCase() === "sap");
84
+ }
85
+
86
+ get existingProductORDId() {
87
+ return this._existingProductORDId;
88
+ }
89
+
90
+ _resolveServiceNames(csn) {
91
+ return Object.keys(csn.definitions) //
92
+ .filter((name) => this._isValidService(name, csn.definitions[name]));
93
+ }
94
+
95
+ _loadNameFromPackageJson() {
96
+ const packageJsonPath = path.join(cds.root, "package.json");
97
+
98
+ if (!cds.utils.exists(packageJsonPath)) {
99
+ throw new Error("package.json not found in the project root directory");
100
+ }
101
+
102
+ return JSON.parse(readFileSync(packageJsonPath, "utf-8")).name;
103
+ }
104
+
105
+ _isBlockedServiceName(name) {
106
+ return ["cds.xt.MTXServices", "MtxOrdProviderService", "OpenResourceDiscoveryService"] //
107
+ .some((blocked) => name.includes(blocked));
108
+ }
109
+
110
+ _resolveApiResourceNames(csn) {
111
+ return Object.keys(csn.definitions)
112
+ .filter((name) => this._isValidService(name, csn.definitions[name]))
113
+ .filter((name) => !this._serviceOnlyContainsEvents(csn.definitions[name]));
114
+ }
115
+
116
+ _resolveEntityTypeTargets(csn) {
117
+ return _.uniqBy(
118
+ Object.keys(csn.definitions)
119
+ .filter((name) => !name.includes(".texts"))
120
+ .filter((name) => !this._isBlockedServiceName(name))
121
+ .filter((name) => csn.definitions[name].kind === CDS_ELEMENT_KIND.entity)
122
+ .filter((name) => csn.definitions[name]["_service"]?.["@protocol"] !== "none")
123
+ .flatMap((name) => createEntityTypeMappingsItemTemplate(csn.definitions[name]) || []),
124
+ CONTENT_MERGE_KEY,
125
+ );
126
+ }
127
+
128
+ _resolveEventServiceNames(csn) {
129
+ const serviceNames = this._resolveServiceNames(csn);
130
+
131
+ return [
132
+ ...new Set(
133
+ Object.keys(csn.definitions)
134
+ .filter((name) => !this._isBlockedServiceName(name))
135
+ .filter((name) => csn.definitions[name].kind === CDS_ELEMENT_KIND.event)
136
+ .filter((name) => csn.definitions[name]["_service"]?.["@protocol"] !== "none")
137
+ .filter((name) => serviceNames.some((serviceName) => name.startsWith(`${serviceName}.`)))
138
+ .map((name) => serviceNames.find((serviceName) => name.startsWith(`${serviceName}.`))),
139
+ ),
140
+ ];
141
+ }
142
+
143
+ _isValidService(key, definition) {
144
+ const isExternalService = Object.keys(cds).includes("requires") && Object.keys(cds.requires).includes(key);
145
+
146
+ return (
147
+ definition.kind === CDS_ELEMENT_KIND.service &&
148
+ !definition["@cds.external"] &&
149
+ definition["@protocol"] !== "none" &&
150
+ !isExternalService &&
151
+ !this._isBlockedServiceName(key)
152
+ );
153
+ }
154
+
155
+ _serviceOnlyContainsEvents(definition) {
156
+ return (
157
+ Object.keys(definition.events || {}).length > 0 &&
158
+ ["actions", "entities", "functions"].every((key) => Object.keys(definition[key] || {}).length === 0)
159
+ );
160
+ }
161
+ };
package/lib/constants.js CHANGED
@@ -8,11 +8,6 @@ const BASIC_AUTH_HEADER_KEY = "authorization";
8
8
 
9
9
  const BUILD_DEFAULT_PATH = "gen/ord";
10
10
 
11
- const BLOCKED_SERVICE_NAME = Object.freeze({
12
- MTXServices: "cds.xt.MTXServices",
13
- OpenResourceDiscoveryService: "OpenResourceDiscoveryService",
14
- });
15
-
16
11
  const ORD_ACCESS_STRATEGY = Object.freeze({
17
12
  Open: "open",
18
13
  Basic: "basic-auth",
@@ -76,8 +71,6 @@ const LEVEL = Object.freeze({
76
71
  subEntity: "sub-entity",
77
72
  });
78
73
 
79
- const OPEN_RESOURCE_DISCOVERY_VERSION = "1.12";
80
-
81
74
  const OPENAPI_SERVERS_ANNOTATION = "@OpenAPI.servers";
82
75
 
83
76
  const ORD_EXTENSIONS_PREFIX = "@ORD.Extensions.";
@@ -89,8 +82,9 @@ const ORD_EXISTING_PRODUCT_PROPERTY = "existingProductORDId";
89
82
  const ORD_RESOURCE_TYPE = Object.freeze({
90
83
  api: "api",
91
84
  event: "event",
92
- integrationDependency: "integrationDependency",
93
85
  entityType: "entityType",
86
+ dataProduct: "dataProduct",
87
+ integrationDependency: "integrationDependency",
94
88
  });
95
89
 
96
90
  const ORD_SERVICE_NAME = "OpenResourceDiscoveryService";
@@ -181,12 +175,18 @@ const AUTH_STRINGS = Object.freeze({
181
175
  WWW_AUTHENTICATE_REALM: 'Basic realm="401"',
182
176
  });
183
177
 
178
+ const DOCUMENT_PERSPECTIVES = Object.freeze({
179
+ SystemVersion: "system-version",
180
+ SystemInstance: "system-instance",
181
+ });
182
+
183
+ const LOCAL_TENANT_ID_HEADER_KEY = "local-tenant-id";
184
+
184
185
  module.exports = {
185
186
  AUTHENTICATION_TYPE,
186
187
  AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,
187
188
  BASIC_AUTH_HEADER_KEY,
188
189
  BUILD_DEFAULT_PATH,
189
- BLOCKED_SERVICE_NAME,
190
190
  CAP_TO_ORD_PROTOCOL_MAP,
191
191
  CDS_ELEMENT_KIND,
192
192
  CF_MTLS_HEADERS,
@@ -202,7 +202,6 @@ module.exports = {
202
202
  INTEGRATION_DEPENDENCY_RESOURCE_NAME,
203
203
  LEVEL,
204
204
  MCP_RESOURCE_DEFINITION_TYPE,
205
- OPEN_RESOURCE_DISCOVERY_VERSION,
206
205
  OPENAPI_SERVERS_ANNOTATION,
207
206
  ORD_ACCESS_STRATEGY,
208
207
  ORD_API_PROTOCOL,
@@ -223,4 +222,6 @@ module.exports = {
223
222
  CF_MTLS_ERROR_REASON,
224
223
  HTTP_CONFIG,
225
224
  AUTH_STRINGS,
225
+ DOCUMENT_PERSPECTIVES,
226
+ LOCAL_TENANT_ID_HEADER_KEY,
226
227
  };
package/lib/defaults.js CHANGED
@@ -1,15 +1,15 @@
1
- const { OPEN_RESOURCE_DISCOVERY_VERSION, SHORT_DESCRIPTION_PREFIX, RESOURCE_VISIBILITY } = require("./constants");
2
- const { hasSAPPolicyLevel } = require("./utils");
1
+ const fs = require("fs");
3
2
  const _ = require("lodash");
3
+ const cds = require("@sap/cds");
4
+ const { join } = require("path");
4
5
 
5
- const packageTypes = [
6
- { tag: "-api", type: "APIs" },
7
- { tag: "-event", type: "Events" },
8
- { tag: "-consumptionBundle", type: "Consumption Bundles" },
9
- { tag: "-integrationDependency", type: "Integration Dependencies" },
10
- { tag: "-entityType", type: "Entity Types" },
11
- { tag: "-dataProduct", type: "Data Products" },
12
- ];
6
+ const {
7
+ ORD_RESOURCE_TYPE,
8
+ SHORT_DESCRIPTION_PREFIX,
9
+ RESOURCE_VISIBILITY,
10
+ DOCUMENT_PERSPECTIVES,
11
+ CONTENT_MERGE_KEY,
12
+ } = require("./constants");
13
13
 
14
14
  const stringTypeCheck = (value) => typeof value === "string";
15
15
  const arrayTypeCheck = (value) => Array.isArray(value);
@@ -77,8 +77,9 @@ const mapEnvPackageInfo = (packageConfig) => {
77
77
  */
78
78
  module.exports = {
79
79
  $schema: "https://open-resource-discovery.github.io/specification/spec-v1/interfaces/Document.schema.json",
80
- openResourceDiscovery: OPEN_RESOURCE_DISCOVERY_VERSION,
80
+ openResourceDiscovery: "1.14",
81
81
  policyLevels: ["none"],
82
+ groupTypeId: "sap.cds:service",
82
83
  description: "this is an application description",
83
84
  products: (name) => [
84
85
  {
@@ -88,40 +89,38 @@ module.exports = {
88
89
  vendor: "customer:vendor:Customer:",
89
90
  },
90
91
  ],
91
- groupTypeId: "sap.cds:service",
92
- packages: function getPackageData(appConfig) {
92
+ packages: (appConfig, products) => {
93
93
  const name = appConfig.appName;
94
94
  const ordNamespace = appConfig.ordNamespace;
95
- const productsOrdId = appConfig.existingProductORDId || appConfig.products?.[0]?.ordId;
95
+ const productsOrdId = appConfig.existingProductORDId || products?.[0]?.ordId;
96
96
  const { vendor, ...envValues } = mapEnvPackageInfo(appConfig?.env?.packages?.[0]);
97
- if (hasSAPPolicyLevel(appConfig.policyLevels)) {
98
- return _.uniqBy(
99
- packageTypes.flatMap(({ tag, type }) =>
100
- Object.values(RESOURCE_VISIBILITY).map((visibility) => ({
101
- ordId: generateUniquePackageOrdId(ordNamespace, name, tag, visibility),
102
- title: nameWithSpaces(name),
103
- ...generatePackageDescriptions(name, type, visibility),
104
- version: "1.0.0",
105
- ...(productsOrdId && { partOfProducts: [productsOrdId || defaultProductOrdId(name)] }),
106
- vendor: vendor || "customer:vendor:Customer:",
107
- ...envValues,
108
- })),
109
- ),
110
- "ordId",
111
- );
112
- } else {
113
- return [
114
- {
115
- ordId: generateUniquePackageOrdId(ordNamespace, name, "", RESOURCE_VISIBILITY.public),
97
+ const visibilities = !appConfig.hasSAPPolicyLevel
98
+ ? [RESOURCE_VISIBILITY.public]
99
+ : Object.values(RESOURCE_VISIBILITY);
100
+ const packageTypes = !appConfig.hasSAPPolicyLevel
101
+ ? [{ tag: "", type: "General" }]
102
+ : [
103
+ { tag: `-${ORD_RESOURCE_TYPE.api}`, type: "APIs" },
104
+ { tag: `-${ORD_RESOURCE_TYPE.event}`, type: "Events" },
105
+ { tag: `-${ORD_RESOURCE_TYPE.entityType}`, type: "Entity Types" },
106
+ { tag: `-${ORD_RESOURCE_TYPE.dataProduct}`, type: "Data Products" },
107
+ { tag: `-${ORD_RESOURCE_TYPE.integrationDependency}`, type: "Integration Dependencies" },
108
+ ];
109
+
110
+ return _.uniqBy(
111
+ packageTypes.flatMap(({ tag, type }) =>
112
+ visibilities.map((visibility) => ({
113
+ ordId: generateUniquePackageOrdId(ordNamespace, name, tag, visibility),
116
114
  title: nameWithSpaces(name),
117
- ...generatePackageDescriptions(name, "General", RESOURCE_VISIBILITY.public),
115
+ ...generatePackageDescriptions(name, type, visibility),
118
116
  version: "1.0.0",
119
117
  ...(productsOrdId && { partOfProducts: [productsOrdId || defaultProductOrdId(name)] }),
120
118
  vendor: vendor || "customer:vendor:Customer:",
121
119
  ...envValues,
122
- },
123
- ];
124
- }
120
+ })),
121
+ ),
122
+ CONTENT_MERGE_KEY,
123
+ );
125
124
  },
126
125
  consumptionBundles: (appConfig) => [
127
126
  {
@@ -134,23 +133,55 @@ module.exports = {
134
133
  "This Consumption Bundle contains all resources of the reference app which are unprotected and do not require authentication",
135
134
  },
136
135
  ],
137
-
138
- apiResources: [],
139
- eventResources: [],
140
- entityTypes: [],
141
- baseTemplate: (authConfig) => {
136
+ baseTemplate: (authConfig, tenant) => {
142
137
  // Get access strategies from the provided authConfig
143
138
  // If auth config is not available, fall back to empty array
139
+ const toggles = cds.env.requires.toggles;
140
+ const extensibility = cds.env.requires.extensibility;
144
141
  const accessStrategies = authConfig?.accessStrategies || [];
142
+
145
143
  return {
146
144
  openResourceDiscoveryV1: {
147
145
  documents: [
148
146
  {
149
147
  url: "/ord/v1/documents/ord-document",
148
+ perspective: DOCUMENT_PERSPECTIVES.SystemVersion,
150
149
  accessStrategies,
151
150
  },
151
+ ...(!tenant || !(toggles || extensibility)
152
+ ? []
153
+ : [
154
+ {
155
+ url: `/ord/v1/documents/ord-document?perspective=${encodeURIComponent(DOCUMENT_PERSPECTIVES.SystemInstance)}`,
156
+ perspective: DOCUMENT_PERSPECTIVES.SystemInstance,
157
+ accessStrategies,
158
+ },
159
+ ]),
152
160
  ],
153
161
  },
154
162
  };
155
163
  },
164
+ adjustForPerspective: (document, perspective) => {
165
+ document.perspective = perspective;
166
+
167
+ if (perspective === DOCUMENT_PERSPECTIVES.SystemVersion) {
168
+ document.describedSystemVersion = cds.env["ord"]?.describedSystemVersion ?? {
169
+ version: JSON.parse(fs.readFileSync(join(cds.root, "package.json"), "utf-8")).version,
170
+ };
171
+ } else if (perspective === DOCUMENT_PERSPECTIVES.SystemInstance) {
172
+ (document.apiResources || []).forEach((apiResource) => {
173
+ (apiResource.resourceDefinitions || []).forEach((apiResourceDefinition) => {
174
+ apiResourceDefinition.url += `?perspective=${encodeURIComponent(perspective)}`;
175
+ });
176
+ });
177
+
178
+ (document.eventResources || []).forEach((eventResource) => {
179
+ (eventResource.resourceDefinitions || []).forEach((eventResourceDefinition) => {
180
+ eventResourceDefinition.url += `?perspective=${encodeURIComponent(perspective)}`;
181
+ });
182
+ });
183
+ }
184
+
185
+ return document;
186
+ },
156
187
  };
@@ -62,13 +62,7 @@ function getCustomORDContent(appConfig) {
62
62
  return require(pathToCustomORDContent);
63
63
  }
64
64
 
65
- function extendCustomORDContentIfExists(appConfig, ordContent) {
66
- const customORDContent = getCustomORDContent(appConfig);
67
- return customORDContent
68
- ? compareAndHandleCustomORDContentWithExistingContent(ordContent, customORDContent)
69
- : ordContent;
70
- }
71
-
72
65
  module.exports = {
73
- extendCustomORDContentIfExists,
66
+ getCustomORDContent,
67
+ compareAndHandleCustomORDContentWithExistingContent,
74
68
  };
package/lib/index.js CHANGED
@@ -7,5 +7,5 @@
7
7
  */
8
8
  module.exports = {
9
9
  ord: require("./ord.js"),
10
- getMetadata: require("./metaData.js"),
10
+ getMetadata: require("./meta-data.js"),
11
11
  };
@@ -122,6 +122,4 @@ module.exports = {
122
122
  // Exported for testing
123
123
  collectExternalServices,
124
124
  createIntegrationDependency,
125
- parseDataProductOrdId,
126
- isExternalDataProduct,
127
125
  };
@@ -54,7 +54,7 @@ function add_i18n_texts(csn) {
54
54
  if (n.startsWith("@")) {
55
55
  let annoVal = o[n];
56
56
  if (typeof annoVal === "string" && annoVal.startsWith("{i18n>")) {
57
- let [, x] = annoVal.match(/^\{i18n>(.*)\}$/) || [];
57
+ let [, x] = annoVal.match(/^\{i18n>(.*)}$/) || [];
58
58
  if (x) keys.add(x);
59
59
  }
60
60
  }
@@ -73,7 +73,7 @@ function remove_localized_assoc(csn) {
73
73
  // - name is "localized"
74
74
  // - type is "cds.Association"
75
75
  // - target name is source name + ".texts"
76
- // - there is an ON condition
76
+ // - there is an ON condition
77
77
  for (let n1 in csn.definitions) {
78
78
  let def = csn.definitions[n1];
79
79
  if (def.kind === "entity")
@@ -141,7 +141,7 @@ function add_meta_info(csn) {
141
141
  for (let k in csn.meta) {
142
142
  if (!["flavor", "document", "features"].includes(k) && !k.startsWith("__")) {
143
143
  delete csn.meta[k];
144
- }
144
+ }
145
145
  }
146
146
  csn.meta.flavor = "effective";
147
147
 
@@ -165,5 +165,5 @@ function add_meta_info(csn) {
165
165
  }
166
166
 
167
167
  module.exports = {
168
- interopCSN,
168
+ interopCSN
169
169
  };
@@ -0,0 +1,90 @@
1
+ const path = require("path");
2
+ const cds = require("@sap/cds/lib");
3
+ const assert = require("node:assert");
4
+ const cdsc = require("@sap/cds-compiler/lib/main");
5
+ const { compile: openapi } = require("@cap-js/openapi");
6
+ const { compile: asyncapi } = require("@cap-js/asyncapi");
7
+
8
+ const Logger = require("./logger");
9
+ const { interopCSN } = require("./interop-csn.js");
10
+ const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");
11
+
12
+ function extractServiceName(url) {
13
+ return path.basename(url, ".json").split(".").slice(0, -1).join(".");
14
+ }
15
+
16
+ function extractCompilerType(url) {
17
+ return path.basename(url, ".json").split(".").pop();
18
+ }
19
+
20
+ const compilers = Object.freeze({
21
+ [COMPILER_TYPES.csn]: function (csn, options) {
22
+ return {
23
+ contentType: "application/json",
24
+ response: interopCSN(
25
+ cdsc.for.effective(csn, { beta: { effectiveCsn: true }, effectiveServiceName: options.service }),
26
+ ),
27
+ };
28
+ },
29
+ [COMPILER_TYPES.mcp]: async function (csn, options) {
30
+ return {
31
+ contentType: "application/json",
32
+ response: cds.compile(csn).to["mcp"]({ ...options, ...(cds.env.ord?.compileOptions?.mcp || {}) }),
33
+ };
34
+ },
35
+ [COMPILER_TYPES.oas3]: function (csn, options) {
36
+ // Check for service-level @OpenAPI.servers annotation
37
+ const servers = csn?.definitions?.[options.service]?.[OPENAPI_SERVERS_ANNOTATION];
38
+ const openapiOptions = { ...options, ...(cds.env?.ord?.compileOptions?.openapi || {}) };
39
+
40
+ // Service-level annotation takes precedence over global config
41
+ if (Array.isArray(servers) && servers.length) {
42
+ openapiOptions["openapi:servers"] = JSON.stringify(servers);
43
+ }
44
+
45
+ return {
46
+ contentType: "application/json",
47
+ response: openapi(csn, openapiOptions),
48
+ };
49
+ },
50
+ [COMPILER_TYPES.edmx]: function (csn, options) {
51
+ return {
52
+ contentType: "application/xml",
53
+ response: cds
54
+ .compile(csn)
55
+ .to["edmx"]({ ...options, ...(cds.env?.ord?.compileOptions?.edmx || {}) }),
56
+ };
57
+ },
58
+ [COMPILER_TYPES.graphql]: function (csn, options) {
59
+ const { generateSchema4 } = require("@cap-js/graphql/lib/schema");
60
+ const { printSchema, lexicographicSortSchema } = require("graphql");
61
+ const srv = new cds.ApplicationService(options.service, cds.linked(csn));
62
+
63
+ return {
64
+ contentType: "text/plain",
65
+ response: printSchema(lexicographicSortSchema(generateSchema4({ [options.service]: srv }))),
66
+ };
67
+ },
68
+ [COMPILER_TYPES.asyncapi2]: function (csn, options) {
69
+ return {
70
+ contentType: "application/json",
71
+ response: asyncapi(csn, { ...options, ...(cds.env?.ord?.compileOptions?.asyncapi || {}) }),
72
+ };
73
+ },
74
+ });
75
+
76
+ module.exports = async (url, model = null) => {
77
+ const name = extractServiceName(url);
78
+ const type = extractCompilerType(url);
79
+ const csn = model || cds.services[name]?.model;
80
+ const options = { service: name, as: "str", messages: [] };
81
+
82
+ assert(Object.hasOwn(compilers, type), `Unsupported format: ${type}`);
83
+
84
+ try {
85
+ return await compilers[type](csn, options);
86
+ } catch(error) {
87
+ Logger.error(`Compilation failed for service ${name} (compiler: ${type}) - ${error.message}`);
88
+ throw error;
89
+ }
90
+ };