@cap-js/ord 1.0.3 → 1.2.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/README.md CHANGED
@@ -4,9 +4,15 @@
4
4
 
5
5
  ## About this project
6
6
 
7
- This plugin enables generation of ORD document for CAP based applications. When you adopt ORD, your application gains a single entry point, known as the Service Provider Interface. This interface allows you to discover and gather relevant information or metadata. You can use this information to construct a static metadata catalog or to perform a detailed runtime inspection of your actual system landscapes.
7
+ This plugin adds support for the [Open Resource Discovery](https://sap.github.io/open-resource-discovery/) (ORD) protocol for CAP based applications.
8
+ When you add the ORD plugin, your application gains a single entry point, which allows to discover and gather machine-readable information or metadata about the application.
9
+ You can use this information to construct a static metadata catalog or to perform a detailed runtime inspection of your actual system instances / system landscapes.
8
10
 
9
- Open Resource Discovery [(ORD)](https://sap.github.io/open-resource-discovery/) is a protocol that enables applications and services to self-describe their available resources and capabilities. It's not only useful for describing static documentation, but it also accurately reflects tenant-specific configurations and extensions at runtime. Typically, ORD is used to describe APIs and Events, but it also supports higher-level concepts like Entity Types (Business Objects) and Data Products (beta).
11
+ For more information, have a look at the [Open Resource Discovery](https://sap.github.io/open-resource-discovery/) page.
12
+
13
+ > ⚠ By installing this plugin, the metadata describing your CAP application will be made openly accessible.
14
+ >
15
+ > If you have a need to protect your metadata, please refrain from installing this plugin until we support metadata protection (planned).
10
16
 
11
17
  ## Requirements and Setup
12
18
 
@@ -21,13 +27,13 @@ npm install @cap-js/ord
21
27
  #### Programmatic API
22
28
 
23
29
  ```js
24
- const cds = require('@sap/cds')
25
- require('@cap-js/ord');
30
+ const cds = require("@sap/cds");
31
+ require("@cap-js/ord");
26
32
  ```
27
33
 
28
34
  ```js
29
- const csn = await cds.load(cds.env.folders.srv)
30
- const ord = cds.compile.to.ord(csn)
35
+ const csn = await cds.load(cds.env.folders.srv);
36
+ const ord = cds.compile.to.ord(csn);
31
37
  ```
32
38
 
33
39
  #### Command Line
@@ -40,24 +46,28 @@ cds compile <path to srv folder> --to ord [-o] [destinationFilePath]
40
46
 
41
47
  #### ORD Endpoints
42
48
 
43
- 1) Run `cds watch` in the application's root.
44
- 2) Check the following relative paths for ORD information - `/.well-known/open-resource-discovery` , `/open-resource-discovery/v1/documents/1`.
45
-
49
+ 1. Run `cds watch` in the application's root.
50
+ 2. Check the following relative paths for ORD information - `/.well-known/open-resource-discovery` , `/open-resource-discovery/v1/documents/1`.
46
51
 
47
52
  <img width="1300" alt="Sample Application Demo" style="border-radius:0.5rem;" src="./asset/etc/ordEndpoint.gif">
48
53
 
49
54
  ### Customizing ORD Document
50
55
 
51
- You can find more information, such as how to customize the ORD Document, in this [document](ord.md).
52
-
56
+ You can find more information, such as how to customize the ORD Document, in this [document](./docs/ord.md).
53
57
 
54
58
  ## Support, Feedback, Contributing
55
59
 
56
60
  This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/ord/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
57
61
 
58
62
  ## Security / Disclosure
63
+
59
64
  If you find any bug that may be a security problem, please follow our instructions at [in our security policy](https://github.com/cap-js/ord/issues/security/policy) on how to report it. Please do not create GitHub issues for security-related doubts or problems.
60
65
 
66
+ At the current state, the plugin will expose static metadata with open access.
67
+ This means that the CAP resources are described and documented openly, but it does not imply that the resources themselves can be accessed.
68
+
69
+ If you have a need to protect your metadata, please refrain from installing this plugin until we support metadata protection.
70
+
61
71
  ## Code of Conduct
62
72
 
63
73
  We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/cap-js/.github/blob/main/CODE_OF_CONDUCT.md) at all times.
@@ -0,0 +1,39 @@
1
+ const COMPILER_TYPES = Object.freeze({
2
+ oas3: "oas3",
3
+ asyncapi2: "asyncapi2",
4
+ edmx: "edmx",
5
+ });
6
+
7
+ const CONTENT_MERGE_KEY = "ordId";
8
+
9
+ const DESCRIPTION_PREFIX = "Description for ";
10
+
11
+ const OPEN_RESOURCE_DISCOVERY_VERSION = "1.9";
12
+
13
+ const ORD_EXTENSIONS_PREFIX = "@ORD.Extensions.";
14
+
15
+ const ORD_RESOURCE_TYPE = Object.freeze({
16
+ service: "service",
17
+ entity: "entity",
18
+ event: "event",
19
+ api: "api",
20
+ });
21
+
22
+ const RESOURCE_VISIBILITY = Object.freeze({
23
+ public: "public",
24
+ internal: "internal",
25
+ private: "private",
26
+ });
27
+
28
+ const SHORT_DESCRIPTION_PREFIX = "Short description for ";
29
+
30
+ module.exports = {
31
+ COMPILER_TYPES,
32
+ CONTENT_MERGE_KEY,
33
+ DESCRIPTION_PREFIX,
34
+ OPEN_RESOURCE_DISCOVERY_VERSION,
35
+ ORD_EXTENSIONS_PREFIX,
36
+ ORD_RESOURCE_TYPE,
37
+ RESOURCE_VISIBILITY,
38
+ SHORT_DESCRIPTION_PREFIX,
39
+ };
package/lib/date.js ADDED
@@ -0,0 +1,22 @@
1
+ function getRFC3339Date(includeOffset = true) {
2
+ const now = new Date();
3
+ const year = now.getUTCFullYear();
4
+ const month = String(now.getUTCMonth() + 1).padStart(2, '0');
5
+ const day = String(now.getUTCDate()).padStart(2, '0');
6
+ const hours = String(now.getUTCHours()).padStart(2, '0');
7
+ const minutes = String(now.getUTCMinutes()).padStart(2, '0');
8
+ const seconds = String(now.getUTCSeconds()).padStart(2, '0');
9
+
10
+ if (includeOffset) {
11
+ const offsetHours = '01';
12
+ const offsetMinutes = '00';
13
+ const offsetSign = '+';
14
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMinutes}`;
15
+ } else {
16
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`;
17
+ }
18
+ }
19
+
20
+ module.exports = {
21
+ getRFC3339Date
22
+ };
package/lib/defaults.js CHANGED
@@ -1,66 +1,87 @@
1
+ const { OPEN_RESOURCE_DISCOVERY_VERSION } = require("./constants");
1
2
 
2
3
  const regexWithRemoval = (name) => {
3
- if(name){
4
- return name.replace(/[^a-zA-Z0-9]/g,'');
5
- }
6
- }
7
- const regexWithUnderScore = (name) => {
8
- return regexWithRemoval(name.charAt(0)) + name.slice(1, name.length).replace(/[^a-zA-Z0-9]/g,'_');
9
- }
4
+ if (name) {
5
+ return name.replace(/[^a-zA-Z0-9]/g, "");
6
+ }
7
+ };
8
+
9
+ const nameWithDot = (name) => {
10
+ return (
11
+ regexWithRemoval(name.charAt(0)) +
12
+ name.slice(1, name.length).replace(/[^a-zA-Z0-9]/g, ".")
13
+ );
14
+ };
15
+
16
+ const nameWithSpaces = (name) => {
17
+ return (
18
+ regexWithRemoval(name.charAt(0)) +
19
+ name.slice(1, name.length).replace(/[^a-zA-Z0-9]/g, " ")
20
+ );
21
+ };
22
+
23
+ const defaultProductOrdId = (name) => `customer:product:${nameWithDot(name)}:`;
10
24
 
11
25
  /**
12
26
  * Module containing default configuration for ORD Document.
13
27
  * @module defaults
14
28
  */
15
29
  module.exports = {
16
- openResourceDiscovery: "1.9",
17
- policyLevel: "none",
18
- description: "this is an application description",
19
- products: (name) => [
20
- {
21
- ordId: `customer:product:${regexWithUnderScore(name)}:`,
22
- title: `ORD App Title for ${regexWithRemoval(name)}`,
23
- shortDescription: " shortDescription for products",
24
- vendor: "customer:vendor:SAPCustomer:",
25
- },
26
- ],
27
- groupTypeId: "sap.cds:service",
28
- packages: function getPackageData (name, policyLevel,capNamespace) {
29
- function createPackage(name, tag) {
30
- return {
31
- ordId: `${regexWithRemoval(name)}:package:${capNamespace}${tag}`,
32
- title: `sample title for ${regexWithRemoval(name)}`,
33
- shortDescription: "Here is the shortDescription for packages",
34
- description: "Here is the description for packages",
35
- version: "1.0.0",
36
- partOfProducts: [`customer:product:${regexWithUnderScore(name)}:`],
37
- vendor: "customer:vendor:SAP:"
38
- };
39
- }
40
-
41
- if(policyLevel.split(':')[0].toLowerCase() === 'sap') {
42
- return [createPackage(name, "-api:v1"), createPackage(name, "-event:v1")];
43
- }
44
- else {
45
- return [createPackage(name, ":v1")];
46
- }
47
- },
48
- consumptionBundles: (name) => [
49
- {
50
- ordId: `${regexWithRemoval(name)}:consumptionBundle:unknown:v1`,
51
- version: "1.0.0",
52
- title: "Unprotected resources",
53
- shortDescription:
54
- "If we have another protected API then it will be another object",
55
- description:
56
- "This Consumption Bundle contains all resources of the reference app which are unprotected and do not require authentication",
30
+ $schema:
31
+ "https://sap.github.io/open-resource-discovery/spec-v1/interfaces/Document.schema.json",
32
+ openResourceDiscovery: OPEN_RESOURCE_DISCOVERY_VERSION,
33
+ policyLevel: "none",
34
+ description: "this is an application description",
35
+ products: (name) => [
36
+ {
37
+ ordId: defaultProductOrdId(name),
38
+ title: nameWithSpaces(name),
39
+ shortDescription: "Description for " + nameWithSpaces(name),
40
+ vendor: "customer:vendor:customer:",
41
+ },
42
+ ],
43
+ groupTypeId: "sap.cds:service",
44
+ packages: function getPackageData(name, policyLevel, ordNamespace) {
45
+ function createPackage(name, tag) {
46
+ return {
47
+ ordId: `${ordNamespace}:package:${regexWithRemoval(name
48
+ )}${tag}`,
49
+ title: nameWithSpaces(name),
50
+ shortDescription:
51
+ "Short description for " + nameWithSpaces(name),
52
+ description: "Description for " + nameWithSpaces(name),
53
+ version: "1.0.0",
54
+ partOfProducts: [defaultProductOrdId(name)],
55
+ vendor: "customer:vendor:Customer:",
56
+ };
57
+ }
58
+
59
+ if (policyLevel.split(":")[0].toLowerCase() === "sap") {
60
+ return [
61
+ createPackage(name, "-api:v1"),
62
+ createPackage(name, "-event:v1"),
63
+ ];
64
+ } else {
65
+ return [createPackage(name, ":v1")];
66
+ }
57
67
  },
58
- ],
59
-
68
+ consumptionBundles: (appConfig) => [
69
+ {
70
+ ordId: `${regexWithRemoval(appConfig.appName)}:consumptionBundle:noAuth:v1`,
71
+ version: "1.0.0",
72
+ lastUpdate: appConfig.lastUpdate,
73
+ title: "Unprotected resources",
74
+ shortDescription:
75
+ "If we have another protected API then it will be another object",
76
+ description:
77
+ "This Consumption Bundle contains all resources of the reference app which are unprotected and do not require authentication",
78
+ },
79
+ ],
80
+
60
81
  apiResources: [],
61
82
  eventResources: [],
62
83
  entityTypes: [],
63
- baseTemplate : {
84
+ baseTemplate: {
64
85
  openResourceDiscoveryV1: {
65
86
  documents: [
66
87
  {
@@ -73,5 +94,5 @@ module.exports = {
73
94
  },
74
95
  ],
75
96
  },
76
- }
77
- }
97
+ },
98
+ };
@@ -0,0 +1,66 @@
1
+ const { CONTENT_MERGE_KEY } = require("./constants");
2
+ const cds = require("@sap/cds");
3
+ const fs = require("fs");
4
+ const { Logger } = require("./logger");
5
+ const path = require("path");
6
+ const _ = require("lodash");
7
+
8
+ function cleanNullProperties(obj) {
9
+ for (const key in obj) {
10
+ if (obj[key] === null) {
11
+ delete obj[key];
12
+ } else if (typeof obj[key] === 'object') {
13
+ cleanNullProperties(obj[key]);
14
+ }
15
+ }
16
+ return obj;
17
+ }
18
+
19
+ function patchGeneratedOrdResources(destinationObj, sourceObj) {
20
+ const destObj = _.keyBy(destinationObj, CONTENT_MERGE_KEY);
21
+ const srcObj = _.keyBy(sourceObj, CONTENT_MERGE_KEY);
22
+ for (const ordId in srcObj) {
23
+ if (ordId in destObj) {
24
+ destObj[ordId] = _.assignWith(structuredClone(destObj[ordId]), structuredClone(srcObj[ordId]));
25
+ } else {
26
+ destObj[ordId] = srcObj[ordId];
27
+ }
28
+ }
29
+ return cleanNullProperties(Object.values(destObj));
30
+ }
31
+
32
+ function compareAndHandleCustomORDContentWithExistingContent(ordContent, customORDContent) {
33
+ const clonedOrdContent = structuredClone(ordContent);
34
+ for (const key in customORDContent) {
35
+ const propertyType = typeof customORDContent[key];
36
+ if (propertyType !== 'object' && propertyType !== 'array') {
37
+ Logger.warn('Found ord top level primitive ord property in customOrdFile:', key, '. Please define it in .cdsrc.json.');
38
+
39
+ continue;
40
+ }
41
+ if (key in ordContent) {
42
+ clonedOrdContent[key] = patchGeneratedOrdResources(clonedOrdContent[key], customORDContent[key]);
43
+ } else {
44
+ clonedOrdContent[key] = customORDContent[key];
45
+ }
46
+ }
47
+ return clonedOrdContent;
48
+ }
49
+
50
+ function getCustomORDContent(appConfig) {
51
+ if (!appConfig.env?.customOrdContentFile) return;
52
+ const pathToCustomORDContent = path.join(cds.root, appConfig.env?.customOrdContentFile);
53
+ if (fs.existsSync(pathToCustomORDContent)) {
54
+ Logger.error('Custom ORD content file not found at', pathToCustomORDContent);
55
+ return require(pathToCustomORDContent);
56
+ }
57
+ }
58
+
59
+ function extendCustomORDContentIfExists(appConfig, ordContent) {
60
+ const customORDContent = getCustomORDContent(appConfig);
61
+ return customORDContent ? compareAndHandleCustomORDContentWithExistingContent(ordContent, customORDContent) : ordContent;
62
+ }
63
+
64
+ module.exports = {
65
+ extendCustomORDContentIfExists,
66
+ };
package/lib/logger.js ADDED
@@ -0,0 +1,11 @@
1
+ const cds = require("@sap/cds");
2
+
3
+ const Logger = cds.log("ord-plugin", {
4
+ level: cds.env.DEBUG || process.env.DEBUG
5
+ ? cds.log.levels?.DEBUG
6
+ : cds.log.levels?.WARN,
7
+ });
8
+
9
+ module.exports = {
10
+ Logger,
11
+ };
package/lib/metaData.js CHANGED
@@ -1,43 +1,42 @@
1
1
  const cds = require('@sap/cds/lib');
2
- const { compile : openapi } = require('@cap-js/openapi')
3
- const { compile : asyncapi } = require('@cap-js/asyncapi');
2
+ const { compile: openapi } = require('@cap-js/openapi')
3
+ const { compile: asyncapi } = require('@cap-js/asyncapi');
4
+ const { COMPILER_TYPES } = require('./constants');
5
+ const { Logger } = require('./logger');
4
6
 
5
- /**
6
- * Retrieves the compiled meta data for the specific service or event according to its compiled type
7
- * @param {string} data
8
- * @returns {string, JSON|XML} contentType, response
9
- */
10
7
  module.exports = async (data) => {
11
8
  const parts = data?.split("/").pop().replace(/\.json$/, '').split(".");
12
9
  const compilerType = parts.pop();
13
10
  const serviceName = parts.join(".");
14
11
  const csn = cds.services[serviceName].model;
15
12
 
16
- let responseFile;
13
+ let responseFile;
17
14
  const options = { service: serviceName, as: 'str', messages: [] }
18
- if (compilerType === "oas3") {
19
- try {
20
- responseFile = openapi(csn, options);
21
- } catch (error) {
22
- console.error("OpenApi error:", error.message);
23
- throw error;
24
- }
25
- } else if (compilerType === "asyncapi2") {
26
- try {
27
- responseFile = asyncapi(csn, options);
28
- } catch (error) {
29
- console.error("AsyncApi error:", error.message);
30
- throw error;
31
- }
32
- } else if (compilerType === "edmx") {
33
- try {
34
- responseFile = await cds.compile(csn).to["edmx"](options);
35
- } catch (error) {
36
- console.error("Edmx error:", error.message);
37
- throw error;
38
- }
15
+ switch (compilerType) {
16
+ case COMPILER_TYPES.oas3:
17
+ try {
18
+ responseFile = openapi(csn, options);
19
+ } catch (error) {
20
+ Logger.error('OpenApi error:', error.message);
21
+ throw error;
22
+ }
23
+ break;
24
+ case COMPILER_TYPES.asyncapi2:
25
+ try {
26
+ responseFile = asyncapi(csn, options);
27
+ } catch (error) {
28
+ Logger.error('AsyncApi error:', error.message);
29
+ throw error;
30
+ }
31
+ break;
32
+ case COMPILER_TYPES.edmx:
33
+ try {
34
+ responseFile = await cds.compile(csn).to["edmx"](options);
35
+ } catch (error) {
36
+ Logger.error('Edmx error:', error.message);
37
+ throw error;
38
+ }
39
39
  }
40
-
41
40
  return {
42
41
  contentType: `application/${compilerType === "edmx" ? "xml" : "json"}`,
43
42
  response: responseFile