@cap-js/ord 1.5.0 → 1.6.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
@@ -6,6 +6,7 @@ const cliProgress = require("cli-progress");
6
6
 
7
7
  const os = require("os");
8
8
  const { ord } = require("./index");
9
+ const registerCompileTargets = require("./common/register-compile-targets");
9
10
  const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME } = require("./constants");
10
11
 
11
12
  module.exports = class OrdBuildPlugin extends cds.build.Plugin {
@@ -16,13 +17,7 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
16
17
  }
17
18
 
18
19
  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
- }
20
+ registerCompileTargets();
26
21
 
27
22
  return this.model()
28
23
  .then((model) => ({ model, document: ord(model) }))
@@ -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) => plugin !== "@cap-js/ord")
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
+ };
@@ -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
  };
@@ -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
+ };
package/lib/ord.js CHANGED
@@ -13,8 +13,11 @@ const {
13
13
  createGroupsTemplateForService,
14
14
  _propagateORDVisibility,
15
15
  } = require("./templates");
16
- const { getIntegrationDependencies } = require("./integrationDependency");
17
- const { extendCustomORDContentIfExists } = require("./extendOrdWithCustom");
16
+ const { getIntegrationDependencies } = require("./integration-dependency");
17
+ const {
18
+ getCustomORDContent,
19
+ compareAndHandleCustomORDContentWithExistingContent,
20
+ } = require("./extend-ord-with-custom");
18
21
  const { getRFC3339Date } = require("./date");
19
22
  const { createAuthConfig } = require("./auth/authentication");
20
23
 
@@ -321,7 +324,7 @@ function _filterUnusedPackages(ordDocument) {
321
324
  return ordDocument.packages.filter((pkg) => usedPackageIds.has(pkg.ordId));
322
325
  }
323
326
 
324
- module.exports = (csn) => {
327
+ module.exports = (csn, extensions = []) => {
325
328
  const linkedCsn = _propagateORDVisibility(cds.linked(csn));
326
329
  const appConfig = initializeAppConfig(linkedCsn);
327
330
 
@@ -358,7 +361,12 @@ module.exports = (csn) => {
358
361
  ordDocument.integrationDependencies = integrationDependencies;
359
362
  }
360
363
 
361
- ordDocument = extendCustomORDContentIfExists(appConfig, ordDocument);
364
+ [...(extensions || []), getCustomORDContent(appConfig)]
365
+ .filter((extension) => !!extension)
366
+ .forEach((extension) => {
367
+ ordDocument = compareAndHandleCustomORDContentWithExistingContent(ordDocument, extension);
368
+ });
369
+
362
370
  ordDocument.packages = _filterUnusedPackages(ordDocument);
363
371
 
364
372
  return ordDocument;
@@ -0,0 +1,21 @@
1
+ @protocol: 'rest'
2
+ @requires: [ 'internal-user' ]
3
+ @path: '/-/cds/ord-provider-service'
4
+ @impl:'@cap-js/ord/lib/services/mtx-ord-provider-service.js'
5
+ service cds.xt.ord.MtxOrdProviderService {
6
+
7
+ action getOrdDocument(
8
+ tenant : String,
9
+ @cds.validate: false
10
+ toggles : array of String,
11
+ for : String enum { nodejs; java; },
12
+ ) returns {};
13
+
14
+ action getOrdResourceDefinition(
15
+ tenant : String,
16
+ @cds.validate: false
17
+ toggles : array of String,
18
+ for : String enum { nodejs; java; },
19
+ resource : String,
20
+ ) returns LargeString;
21
+ }
@@ -0,0 +1,39 @@
1
+ const cds = require("@sap/cds/lib");
2
+ const { ord, getMetadata } = require("@cap-js/ord/lib");
3
+
4
+ module.exports = class MtxOrdProviderService extends cds.ApplicationService {
5
+ init() {
6
+ this.on("getOrdDocument", async (req) => {
7
+ req._?.res?.set("Content-Type", "application/json");
8
+
9
+ return ord(
10
+ await (
11
+ await cds.connect.to("cds.xt.ModelProviderService")
12
+ ).getCsn({
13
+ for: req.data.for,
14
+ tenant: req.data.tenant,
15
+ toggles: req.data.toggles,
16
+ }),
17
+ );
18
+ });
19
+
20
+ this.on("getOrdResourceDefinition", async (req) => {
21
+ const { response, contentType } = await getMetadata(
22
+ req.data.resource,
23
+ await (
24
+ await cds.connect.to("cds.xt.ModelProviderService")
25
+ ).getCsn({
26
+ for: req.data.for,
27
+ tenant: req.data.tenant,
28
+ toggles: req.data.toggles,
29
+ }),
30
+ );
31
+
32
+ req._?.res?.set("Content-Type", contentType);
33
+
34
+ return response;
35
+ });
36
+
37
+ return super.init();
38
+ }
39
+ };
@@ -1,12 +1,21 @@
1
1
  const cds = require("@sap/cds");
2
- const ord = require("./ord.js");
3
- const compileMetadata = require("./metaData.js");
4
- const defaults = require("./defaults.js");
5
- const { createAuthConfig, createAuthMiddleware } = require("./auth/authentication.js");
6
- const Logger = require("./logger.js");
2
+
3
+ const ord = require("../ord.js");
4
+ const Logger = require("../logger.js");
5
+ const defaults = require("../defaults.js");
6
+ const compileMetadata = require("../meta-data.js");
7
+ const { createAuthConfig, createAuthMiddleware } = require("../auth/authentication.js");
7
8
 
8
9
  class OpenResourceDiscoveryService extends cds.ApplicationService {
9
10
  async init() {
11
+ this.extensions = {};
12
+
13
+ cds.on("ord.extension.publish", ({ id, data }) => {
14
+ Logger.info(`Registering extension with id ${id}...`);
15
+
16
+ this.extensions[id] = data;
17
+ });
18
+
10
19
  // Initialize authentication configuration from .cdsrc.json or environment variables
11
20
  // CF mTLS validator is lazily initialized on first mTLS request
12
21
  const authConfig = createAuthConfig();
@@ -23,7 +32,7 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
23
32
 
24
33
  cds.app.get(`/ord/v1/documents/ord-document`, authMiddleware, async (_, res) => {
25
34
  const csn = cds.context?.model || cds.model;
26
- const data = ord(csn);
35
+ const data = ord(csn, Array.from(Object.values(this.extensions)));
27
36
  return res.status(200).send(data);
28
37
  });
29
38
 
@@ -1,6 +1,9 @@
1
1
  const { workerData } = require("piscina");
2
2
 
3
- const compileMetadata = require("../metaData");
3
+ const compileMetadata = require("../meta-data");
4
+ const registerCompileTargets = require("../common/register-compile-targets");
5
+
6
+ registerCompileTargets(); // Worker threads skip cds-plugin.js — re-register compile targets
4
7
 
5
8
  module.exports = ({ url }) => {
6
9
  // JSON round-trip: CDS compiler output may contain Generator objects
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/ord",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "CAP Plugin for generating ORD document.",
5
5
  "repository": "cap-js/ord",
6
6
  "author": "SAP SE (https://www.sap.com)",
@@ -34,7 +34,7 @@
34
34
  "eslint": "^10.0.0",
35
35
  "express": "^4",
36
36
  "jest": "^30.0.0",
37
- "prettier": "3.8.1",
37
+ "prettier": "3.8.3",
38
38
  "supertest": "^7.0.0"
39
39
  },
40
40
  "peerDependencies": {
@@ -49,18 +49,23 @@
49
49
  "piscina": "^5.1.4"
50
50
  },
51
51
  "engines": {
52
- "node": ">=18 <23"
52
+ "node": ">=20 <25"
53
53
  },
54
54
  "overrides": {
55
- "@sap/cds-compiler": "6.7.3"
55
+ "@sap/cds-compiler": "6.9.0"
56
56
  },
57
57
  "cds": {
58
58
  "requires": {
59
59
  "SAP ORD Service": {
60
- "model": "@cap-js/ord/lib/ord-service"
60
+ "model": "@cap-js/ord/lib/services/ord-service"
61
61
  },
62
62
  "[java]": {
63
63
  "SAP ORD Service": false
64
+ },
65
+ "[mtx-sidecar]": {
66
+ "SAP ORD Service": {
67
+ "model": "@cap-js/ord/lib/services/mtx-ord-provider-service"
68
+ }
64
69
  }
65
70
  }
66
71
  }
package/lib/metaData.js DELETED
@@ -1,108 +0,0 @@
1
- const cds = require("@sap/cds/lib");
2
- const { compile: openapi } = require("@cap-js/openapi");
3
- const { compile: asyncapi } = require("@cap-js/asyncapi");
4
- const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");
5
- const Logger = require("./logger");
6
- const { interopCSN } = require("./interopCsn.js");
7
- const cdsc = require("@sap/cds-compiler/lib/main");
8
-
9
- /**
10
- * Read @OpenAPI.servers annotation from service definition
11
- * @param {object} csn - The CSN model
12
- * @param {string} serviceName - The service name
13
- * @returns {string|undefined} - JSON string of servers array or undefined
14
- */
15
- const _getServersFromAnnotation = (csn, serviceName) => {
16
- const servers = csn?.definitions?.[serviceName]?.[OPENAPI_SERVERS_ANNOTATION];
17
- const isValidServers = Array.isArray(servers) && servers.length > 0;
18
- return isValidServers ? JSON.stringify(servers) : undefined;
19
- };
20
-
21
- const getMetadata = async (url, model = null) => {
22
- const parts = url
23
- ?.split("/")
24
- .pop()
25
- .replace(/\.json$/, "")
26
- .split(".");
27
- const compilerType = parts.pop();
28
- const serviceName = parts.join(".");
29
- const csn = model || cds.services[serviceName]?.model;
30
- const compileOptions = cds.env["ord"]?.compileOptions || {};
31
-
32
- let responseFile;
33
- const options = { service: serviceName, as: "str", messages: [] };
34
- switch (compilerType) {
35
- case COMPILER_TYPES.oas3:
36
- try {
37
- // Check for service-level @OpenAPI.servers annotation
38
- const serversFromAnnotation = _getServersFromAnnotation(csn, serviceName);
39
- const openapiOptions = { ...options, ...(compileOptions?.openapi || {}) };
40
-
41
- // Service-level annotation takes precedence over global config
42
- if (serversFromAnnotation) {
43
- openapiOptions["openapi:servers"] = serversFromAnnotation;
44
- }
45
-
46
- responseFile = openapi(csn, openapiOptions);
47
- } catch (error) {
48
- Logger.error(`OpenApi error for service ${serviceName} - ${error.message}`);
49
- throw error;
50
- }
51
- break;
52
- case COMPILER_TYPES.asyncapi2:
53
- try {
54
- responseFile = asyncapi(csn, { ...options, ...(compileOptions?.asyncapi || {}) });
55
- } catch (error) {
56
- Logger.error(`AsyncApi error for service ${serviceName} - ${error.message}`);
57
- throw error;
58
- }
59
- break;
60
- case COMPILER_TYPES.csn:
61
- try {
62
- const opt_eff = { beta: { effectiveCsn: true }, effectiveServiceName: serviceName };
63
- let effCsn = cdsc.for.effective(csn, opt_eff);
64
- responseFile = interopCSN(effCsn);
65
- } catch (error) {
66
- Logger.error(`Csn error for service ${serviceName} - ${error.message}`);
67
- throw error;
68
- }
69
- break;
70
- case COMPILER_TYPES.edmx:
71
- try {
72
- responseFile = await cds.compile(csn).to["edmx"]({ ...options, ...(compileOptions?.edmx || {}) });
73
- } catch (error) {
74
- Logger.error(`Edmx error for service ${serviceName} - ${error.message}`);
75
- throw error;
76
- }
77
- break;
78
- case COMPILER_TYPES.mcp:
79
- try {
80
- responseFile = await cds.compile(csn).to["mcp"]({ ...options, ...(compileOptions?.mcp || {}) });
81
- } catch (error) {
82
- Logger.error("MCP error:", error.message);
83
- throw error;
84
- }
85
- break;
86
- case COMPILER_TYPES.graphql:
87
- try {
88
- const { generateSchema4 } = require("@cap-js/graphql/lib/schema");
89
- const { lexicographicSortSchema, printSchema } = require("graphql");
90
- const linked = cds.linked(csn);
91
- const srv = new cds.ApplicationService(serviceName, linked);
92
- let schema = generateSchema4({ [serviceName]: srv });
93
- schema = lexicographicSortSchema(schema);
94
- responseFile = printSchema(schema);
95
- } catch (error) {
96
- Logger.error("GraphQL SDL error:", error.message);
97
- throw error;
98
- }
99
- break;
100
- }
101
- return {
102
- contentType:
103
- compilerType === "graphql" ? "text/plain" : `application/${compilerType === "edmx" ? "xml" : "json"}`,
104
- response: responseFile,
105
- };
106
- };
107
-
108
- module.exports = getMetadata;