@cap-js/ord 1.0.3 → 1.1.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 +21 -11
- package/lib/constants.js +25 -0
- package/lib/defaults.js +72 -54
- package/lib/extendOrdWithCustom.js +67 -0
- package/lib/metaData.js +28 -30
- package/lib/ord.js +127 -163
- package/lib/templates.js +155 -282
- package/package.json +33 -26
package/README.md
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
## About this project
|
|
6
6
|
|
|
7
|
-
This plugin
|
|
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
|
|
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(
|
|
25
|
-
require(
|
|
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
|
|
44
|
-
2
|
|
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.
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const ORD_RESOURCE_TYPE = Object.freeze(
|
|
2
|
+
{
|
|
3
|
+
"service": "service",
|
|
4
|
+
"entity": "entity",
|
|
5
|
+
"event": "event",
|
|
6
|
+
"api": "api"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const COMPILER_TYPES = Object.freeze(
|
|
10
|
+
{
|
|
11
|
+
"oas3": "oas3",
|
|
12
|
+
"asyncapi2": "asyncapi2",
|
|
13
|
+
"edmx": "edmx"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const CONTENT_MERGE_KEY = "ordId";
|
|
17
|
+
|
|
18
|
+
const ORD_EXTENSIONS_PREFIX = "@ORD.Extensions.";
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
ORD_RESOURCE_TYPE,
|
|
22
|
+
COMPILER_TYPES,
|
|
23
|
+
CONTENT_MERGE_KEY,
|
|
24
|
+
ORD_EXTENSIONS_PREFIX
|
|
25
|
+
};
|
package/lib/defaults.js
CHANGED
|
@@ -1,66 +1,84 @@
|
|
|
1
|
-
|
|
2
1
|
const regexWithRemoval = (name) => {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
if (name) {
|
|
3
|
+
return name.replace(/[^a-zA-Z0-9]/g, "");
|
|
4
|
+
}
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const nameWithDot = (name) => {
|
|
8
|
+
return (
|
|
9
|
+
regexWithRemoval(name.charAt(0)) +
|
|
10
|
+
name.slice(1, name.length).replace(/[^a-zA-Z0-9]/g, ".")
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const nameWithSpaces = (name) => {
|
|
15
|
+
return (
|
|
16
|
+
regexWithRemoval(name.charAt(0)) +
|
|
17
|
+
name.slice(1, name.length).replace(/[^a-zA-Z0-9]/g, " ")
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const defaultProductOrdId = (name) => `customer:product:${nameWithDot(name)}:`;
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Module containing default configuration for ORD Document.
|
|
13
25
|
* @module defaults
|
|
14
26
|
*/
|
|
15
27
|
module.exports = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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",
|
|
28
|
+
$schema:
|
|
29
|
+
"https://sap.github.io/open-resource-discovery/spec-v1/interfaces/Document.schema.json",
|
|
30
|
+
openResourceDiscovery: "1.9",
|
|
31
|
+
policyLevel: "none",
|
|
32
|
+
description: "this is an application description",
|
|
33
|
+
products: (name) => [
|
|
34
|
+
{
|
|
35
|
+
ordId: defaultProductOrdId(name),
|
|
36
|
+
title: nameWithSpaces(name),
|
|
37
|
+
shortDescription: "Description for " + nameWithSpaces(name),
|
|
38
|
+
vendor: "customer:vendor:customer:",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
groupTypeId: "sap.cds:service",
|
|
42
|
+
packages: function getPackageData(name, policyLevel, ordNamespace) {
|
|
43
|
+
function createPackage(name, tag) {
|
|
44
|
+
return {
|
|
45
|
+
ordId: `${ordNamespace}:package:${regexWithRemoval(name
|
|
46
|
+
)}${tag}`,
|
|
47
|
+
title: nameWithSpaces(name),
|
|
48
|
+
shortDescription:
|
|
49
|
+
"Short description for " + nameWithSpaces(name),
|
|
50
|
+
description: "Description for " + nameWithSpaces(name),
|
|
51
|
+
version: "1.0.0",
|
|
52
|
+
partOfProducts: [defaultProductOrdId(name)],
|
|
53
|
+
vendor: "customer:vendor:Customer:",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (policyLevel.split(":")[0].toLowerCase() === "sap") {
|
|
58
|
+
return [
|
|
59
|
+
createPackage(name, "-api:v1"),
|
|
60
|
+
createPackage(name, "-event:v1"),
|
|
61
|
+
];
|
|
62
|
+
} else {
|
|
63
|
+
return [createPackage(name, ":v1")];
|
|
64
|
+
}
|
|
57
65
|
},
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
consumptionBundles: (name) => [
|
|
67
|
+
{
|
|
68
|
+
ordId: `${regexWithRemoval(name)}:consumptionBundle:unknown:v1`,
|
|
69
|
+
version: "1.0.0",
|
|
70
|
+
title: "Unprotected resources",
|
|
71
|
+
shortDescription:
|
|
72
|
+
"If we have another protected API then it will be another object",
|
|
73
|
+
description:
|
|
74
|
+
"This Consumption Bundle contains all resources of the reference app which are unprotected and do not require authentication",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
|
|
60
78
|
apiResources: [],
|
|
61
79
|
eventResources: [],
|
|
62
80
|
entityTypes: [],
|
|
63
|
-
baseTemplate
|
|
81
|
+
baseTemplate: {
|
|
64
82
|
openResourceDiscoveryV1: {
|
|
65
83
|
documents: [
|
|
66
84
|
{
|
|
@@ -73,5 +91,5 @@ module.exports = {
|
|
|
73
91
|
},
|
|
74
92
|
],
|
|
75
93
|
},
|
|
76
|
-
}
|
|
77
|
-
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const cds = require("@sap/cds");
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const { CONTENT_MERGE_KEY } = require('./constants');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function cleanNullProperties(obj) {
|
|
8
|
+
for (const key in obj) {
|
|
9
|
+
if (obj[key] === null) {
|
|
10
|
+
delete obj[key];
|
|
11
|
+
} else if (typeof obj[key] === 'object') {
|
|
12
|
+
cleanNullProperties(obj[key]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return obj;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function patchGeneratedOrdResources(destinationObj, sourceObj) {
|
|
19
|
+
const destObj = _.keyBy(destinationObj, CONTENT_MERGE_KEY);
|
|
20
|
+
const srcObj = _.keyBy(sourceObj, CONTENT_MERGE_KEY);
|
|
21
|
+
for (const ordId in srcObj) {
|
|
22
|
+
if (ordId in destObj) {
|
|
23
|
+
destObj[ordId] = _.assignWith(structuredClone(destObj[ordId]), structuredClone(srcObj[ordId]));
|
|
24
|
+
} else {
|
|
25
|
+
destObj[ordId] = srcObj[ordId];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return cleanNullProperties(Object.values(destObj));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compareAndHandleCustomORDContentWithExistingContent(ordContent, customORDContent, logger) {
|
|
32
|
+
const clonedOrdContent = structuredClone(ordContent);
|
|
33
|
+
for (const key in customORDContent) {
|
|
34
|
+
const propertyType = typeof customORDContent[key];
|
|
35
|
+
if (propertyType !== 'object' && propertyType !== 'array') {
|
|
36
|
+
logger.warn('Found ord top level primitive ord property in customOrdFile:', key, ', please define it in .cdsrc.json.');
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (key in ordContent) {
|
|
40
|
+
clonedOrdContent[key] = patchGeneratedOrdResources(clonedOrdContent[key], customORDContent[key]);
|
|
41
|
+
} else {
|
|
42
|
+
clonedOrdContent[key] = customORDContent[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return clonedOrdContent;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getCustomORDContent(appConfig) {
|
|
49
|
+
if (appConfig.env?.customOrdContentFile) {
|
|
50
|
+
const customORDContent = require(path.join(cds.root, appConfig.env.customOrdContentFile));
|
|
51
|
+
return customORDContent;
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extendCustomORDContentIfExists(appConfig, ordContent, logger) {
|
|
57
|
+
const customORDContent = getCustomORDContent(appConfig);
|
|
58
|
+
|
|
59
|
+
if (customORDContent) {
|
|
60
|
+
ordContent = compareAndHandleCustomORDContentWithExistingContent(ordContent, customORDContent, logger);
|
|
61
|
+
}
|
|
62
|
+
return ordContent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
extendCustomORDContentIfExists,
|
|
67
|
+
};
|
package/lib/metaData.js
CHANGED
|
@@ -1,43 +1,41 @@
|
|
|
1
1
|
const cds = require('@sap/cds/lib');
|
|
2
|
-
const { compile
|
|
3
|
-
const { compile
|
|
2
|
+
const { compile: openapi } = require('@cap-js/openapi')
|
|
3
|
+
const { compile: asyncapi } = require('@cap-js/asyncapi');
|
|
4
|
+
const { COMPILER_TYPES } = require('./constants');
|
|
4
5
|
|
|
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
6
|
module.exports = async (data) => {
|
|
11
7
|
const parts = data?.split("/").pop().replace(/\.json$/, '').split(".");
|
|
12
8
|
const compilerType = parts.pop();
|
|
13
9
|
const serviceName = parts.join(".");
|
|
14
10
|
const csn = cds.services[serviceName].model;
|
|
15
11
|
|
|
16
|
-
let responseFile;
|
|
12
|
+
let responseFile;
|
|
17
13
|
const options = { service: serviceName, as: 'str', messages: [] }
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
14
|
+
switch (compilerType) {
|
|
15
|
+
case COMPILER_TYPES.oas3:
|
|
16
|
+
try {
|
|
17
|
+
responseFile = openapi(csn, options);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error("OpenApi error:", error.message);
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
case COMPILER_TYPES.asyncapi2:
|
|
24
|
+
try {
|
|
25
|
+
responseFile = asyncapi(csn, options);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error("AsyncApi error:", error.message);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
case COMPILER_TYPES.edmx:
|
|
32
|
+
try {
|
|
33
|
+
responseFile = await cds.compile(csn).to["edmx"](options);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Edmx error:", error.message);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
39
38
|
}
|
|
40
|
-
|
|
41
39
|
return {
|
|
42
40
|
contentType: `application/${compilerType === "edmx" ? "xml" : "json"}`,
|
|
43
41
|
response: responseFile
|
package/lib/ord.js
CHANGED
|
@@ -1,202 +1,166 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const cds = require("@sap/cds");
|
|
3
3
|
const { exists } = cds.utils;
|
|
4
|
+
const _ = require("lodash");
|
|
4
5
|
const defaults = require("./defaults");
|
|
5
6
|
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
fCreateEntityTypeTemplate
|
|
7
|
+
createAPIResourceTemplate,
|
|
8
|
+
createEventResourceTemplate,
|
|
9
|
+
createGroupsTemplateForService,
|
|
10
|
+
createEntityTypeTemplate
|
|
11
11
|
} = require('./templates');
|
|
12
|
+
const { extendCustomORDContentIfExists } = require('./extendOrdWithCustom');
|
|
13
|
+
const { ORD_RESOURCE_TYPE } = require('./constants');
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
const fInitializeGlobal = (csn) => {
|
|
19
|
-
let packagejsonPath = path.join(cds.root,'package.json')
|
|
15
|
+
const logger = cds.log('ord-plugin');
|
|
16
|
+
|
|
17
|
+
const initializeAppConfig = (csn) => {
|
|
18
|
+
let packagejsonPath = path.join(cds.root, 'package.json')
|
|
20
19
|
let packageJson;
|
|
21
20
|
if (exists(packagejsonPath)) {
|
|
22
21
|
packageJson = require(packagejsonPath);
|
|
23
22
|
} else {
|
|
24
23
|
throw new Error(`package.json not found in the project root directory`);
|
|
25
24
|
}
|
|
25
|
+
const packageName = packageJson.name;
|
|
26
26
|
const appName = packageJson.name.replace(/^[@]/, "").replace(/[@/]/g, "-");
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// namespace variable value if present in cdsrc.json take it there or else from package.json
|
|
27
|
+
const modelKeys = Object.keys(csn.definitions);
|
|
28
|
+
const events = [];
|
|
29
|
+
const serviceNames = [];
|
|
30
|
+
let odmEntity = [];
|
|
31
|
+
|
|
32
|
+
// namespace variable value if present in cdsrc.json take it there or else from package.json
|
|
34
33
|
//if cdsrc.json does not have applicationNamespace, then use just the namespace
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (applicationNamespace && fGetNamespaceComponents(namespace) !== fGetNamespaceComponents(applicationNamespace)) {
|
|
39
|
-
console.warn('ORD and AsyncAPI namespaces should be the same.');
|
|
40
|
-
}
|
|
34
|
+
const vendorNamespace = "customer";
|
|
35
|
+
const ordNamespace = cds.env["ord"]?.namespace || `${vendorNamespace}.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
36
|
+
const eventApplicationNamespace = cds.env?.export?.asyncapi?.applicationNamespace;
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const fODMEntityFilter = (key, keyDefinition) => {
|
|
45
|
-
return keyDefinition.kind === "entity"
|
|
46
|
-
&& key.includes(capNamespace)
|
|
47
|
-
&& !key.includes(".texts")
|
|
48
|
-
&& keyDefinition["@ODM.entityName"];
|
|
38
|
+
if (eventApplicationNamespace && ordNamespace !== eventApplicationNamespace) {
|
|
39
|
+
console.warn("ORD and AsyncAPI namespaces should be the same.");
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
|
|
42
|
+
for (const key of modelKeys) {
|
|
52
43
|
const keyDefinition = csn.definitions[key];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
switch (keyDefinition.kind) {
|
|
45
|
+
case ORD_RESOURCE_TYPE.service:
|
|
46
|
+
if (!keyDefinition["@cds.external"]) {
|
|
47
|
+
serviceNames.push(key);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
// TODO: should be rewritten
|
|
51
|
+
case ORD_RESOURCE_TYPE.entity:
|
|
52
|
+
if (!key.includes(".texts") && keyDefinition["@ODM.entityName"]) {
|
|
53
|
+
odmEntity.push(createEntityTypeTemplate(keyDefinition));
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case ORD_RESOURCE_TYPE.event:
|
|
57
|
+
events.push(key);
|
|
58
|
+
break;
|
|
59
59
|
}
|
|
60
|
-
}
|
|
60
|
+
}
|
|
61
61
|
|
|
62
|
+
odmEntity = _.uniqBy(odmEntity, "ordId");
|
|
62
63
|
|
|
63
64
|
return {
|
|
64
65
|
env: cds.env["ord"],
|
|
65
|
-
capNamespace,
|
|
66
66
|
appName,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
67
|
+
events,
|
|
68
|
+
serviceNames,
|
|
69
|
+
odmEntity,
|
|
70
|
+
ordNamespace,
|
|
71
|
+
eventApplicationNamespace,
|
|
72
|
+
packageName,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const _getPolicyLevel = (appConfig) =>
|
|
77
|
+
appConfig.env?.policyLevel || defaults.policyLevel;
|
|
78
|
+
|
|
79
|
+
const _getDescription = (appConfig) =>
|
|
80
|
+
appConfig.env?.description || defaults.description;
|
|
81
|
+
|
|
82
|
+
const _getProducts = (appConfig) =>
|
|
83
|
+
appConfig.env?.products || defaults.products(appConfig.packageName);
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*/
|
|
80
|
-
const fGetNamespaceComponents = (namespace) => namespace.split('.').slice(0, 2).join('.');
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Retrieves the policy level.
|
|
84
|
-
* Hierarchy to check data: cdsrc.json > defaults
|
|
85
|
-
* @returns {string} The policy level.
|
|
86
|
-
*/
|
|
87
|
-
const fGetPolicyLevel = (global) => global.env?.policyLevel || defaults.policyLevel;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Retrieves the root level ORD document description.
|
|
91
|
-
* Hierarchy to check data: cdsrc.json > defaults
|
|
92
|
-
* @returns {string} The ORD document description.
|
|
93
|
-
*/
|
|
94
|
-
const fGetDescription = (global) => global.env?.description || defaults.description;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Retrieves the products.
|
|
98
|
-
* Hierarchy to check data: cdsrc.json > defaults
|
|
99
|
-
* @returns {Array<object>} The products array.
|
|
100
|
-
* if global.namespace is defined in cdsrc.json, then use it, else use the appName from package.json
|
|
101
|
-
*/
|
|
102
|
-
const fGetProducts = (global) => global.env?.products || defaults.products(global.namespace);
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Retrieves the groups that services belongs to.
|
|
106
|
-
* Gets list of groups from CDS runtime object.
|
|
107
|
-
* @param {Object} csn object
|
|
108
|
-
* @returns {Array<object>} The groups array.
|
|
109
|
-
*/
|
|
110
|
-
const fGetGroups = (csn, global) => {
|
|
111
|
-
// storing the group ids in a set to avoid duplicates
|
|
112
|
-
let groupIds = new Set();
|
|
113
|
-
|
|
114
|
-
let serviceGroups = global.aServices
|
|
115
|
-
.map((srv) => fCreateGroupsTemplateForService(srv, csn.definitions[srv], groupIds))
|
|
116
|
-
.filter((resource) => resource !== null && resource !== undefined);
|
|
117
|
-
let eventGroups = global.aEvents
|
|
118
|
-
.map((event) => fCreateGroupsTemplateForEvent(event, csn.definitions[event], groupIds))
|
|
119
|
-
.filter((resource) => resource !== null && resource !== undefined);
|
|
120
|
-
|
|
121
|
-
return [...serviceGroups, ...eventGroups];
|
|
85
|
+
const _getGroups = (csn, appConfig) => {
|
|
86
|
+
return appConfig.serviceNames
|
|
87
|
+
.flatMap((serviceName) => createGroupsTemplateForService(serviceName, csn.definitions[serviceName], appConfig))
|
|
88
|
+
.filter((resource) => !!resource);
|
|
122
89
|
};
|
|
123
90
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
*/
|
|
136
|
-
const fGetConsumptionBundles = (global) => global.env?.consumptionBundles || (global.aEvents.length) ? defaults.consumptionBundles(global.capNamespace) : defaults.consumptionBundles(global.capNamespace).slice(0,1)
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Retrieves the API Resources
|
|
140
|
-
* Gets list of services from CSN object
|
|
141
|
-
* @param {Object} csn object
|
|
142
|
-
* @returns {Array<object>} The API Resources array.
|
|
143
|
-
*/
|
|
144
|
-
const fGetAPIResources = (csn, global,packageIds) => {
|
|
145
|
-
const apiResources = [];
|
|
146
|
-
global.aServices.forEach((srv) => {
|
|
147
|
-
fCreateAPIResourceTemplate(srv, csn.definitions[srv], global,packageIds)?.forEach(
|
|
148
|
-
(resource) => {
|
|
149
|
-
if (resource !== null && resource !== undefined) {
|
|
150
|
-
apiResources.push(resource);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
return apiResources;
|
|
91
|
+
const _getPackages = (policyLevel, appConfig) =>
|
|
92
|
+
appConfig.env?.packages || appConfig.events.length
|
|
93
|
+
? defaults.packages(appConfig.appName, policyLevel, appConfig.ordNamespace)
|
|
94
|
+
: defaults
|
|
95
|
+
.packages(appConfig.appName, policyLevel, appConfig.ordNamespace)
|
|
96
|
+
.slice(0, 1);
|
|
97
|
+
|
|
98
|
+
const _getAPIResources = (csn, appConfig, packageIds) => {
|
|
99
|
+
return appConfig.serviceNames
|
|
100
|
+
.flatMap((serviceName) => createAPIResourceTemplate(serviceName, csn.definitions[serviceName], appConfig, packageIds) || [])
|
|
101
|
+
.filter((resource) => !!resource);
|
|
156
102
|
};
|
|
157
103
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const fGetEventResources = (csn, global,packageIds) => global.aEvents.map((srv) => fCreateEventResourceTemplate(srv, csn.definitions[srv], global, packageIds)).filter((resource) => resource !== null && resource !== undefined);
|
|
104
|
+
const _getEventResources = (csn, appConfig, packageIds) => {
|
|
105
|
+
if (appConfig.events.length === 0) return [];
|
|
106
|
+
return appConfig.serviceNames
|
|
107
|
+
.filter((serviceName) => appConfig.events.some((eventName) => eventName.startsWith(serviceName)))
|
|
108
|
+
.flatMap((serviceName) => createEventResourceTemplate(serviceName, csn.definitions[serviceName], appConfig, packageIds) || []);
|
|
109
|
+
};
|
|
165
110
|
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
111
|
+
function validateNamespace(appConfig) {
|
|
112
|
+
const validateSystemNamespace = new RegExp(`^${appConfig.eventApplicationNamespace}\\.[^.]+\\..+$`);
|
|
113
|
+
if (
|
|
114
|
+
appConfig.ordNamespace === undefined &&
|
|
115
|
+
!validateSystemNamespace.test(appConfig.ordNamespace)
|
|
116
|
+
) {
|
|
117
|
+
let error = new Error(
|
|
118
|
+
`Namespace is not defined in cdsrc.json or it is not in the format of ${appConfig.eventApplicationNamespace}.<appName>.<service>`
|
|
119
|
+
);
|
|
120
|
+
console.error("Namespace error:", error.message);
|
|
173
121
|
throw error;
|
|
174
122
|
}
|
|
123
|
+
}
|
|
175
124
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
125
|
+
function createDefaultORDDocument(linkedCsn, appConfig) {
|
|
126
|
+
let ordDocument = {
|
|
127
|
+
$schema: "https://sap.github.io/open-resource-discovery/spec-v1/interfaces/Document.schema.json",
|
|
128
|
+
openResourceDiscovery: "1.9",
|
|
129
|
+
policyLevel: _getPolicyLevel(appConfig),
|
|
130
|
+
description: _getDescription(appConfig),
|
|
131
|
+
products: _getProducts(appConfig),
|
|
132
|
+
groups: _getGroups(linkedCsn, appConfig),
|
|
181
133
|
};
|
|
182
|
-
if(fGetAPIResources(linkedCsn, global).length > 0 && (fGetEventResources(linkedCsn, global).length>0)){
|
|
183
|
-
oReturn.packages = fGetPackages(oReturn.policyLevel, global);
|
|
184
|
-
}
|
|
185
134
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if(oReturn.packages){
|
|
189
|
-
oReturn.packages.forEach((pkg) => {
|
|
190
|
-
packageIds.add(pkg.ordId);
|
|
191
|
-
});
|
|
135
|
+
if (_getAPIResources(linkedCsn, appConfig).length && _getEventResources(linkedCsn, appConfig).length) {
|
|
136
|
+
ordDocument.packages = _getPackages(ordDocument.policyLevel, appConfig);
|
|
192
137
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if(
|
|
199
|
-
|
|
138
|
+
return ordDocument;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractPackageIds(ordDocument) {
|
|
142
|
+
const packageIds = new Set();
|
|
143
|
+
if (ordDocument.packages) {
|
|
144
|
+
ordDocument.packages.map((pkg) => packageIds.add(pkg.ordId));
|
|
200
145
|
}
|
|
201
|
-
return
|
|
146
|
+
return packageIds;
|
|
202
147
|
}
|
|
148
|
+
|
|
149
|
+
module.exports = (csn) => {
|
|
150
|
+
const linkedCsn = cds.linked(csn);
|
|
151
|
+
const appConfig = initializeAppConfig(linkedCsn);
|
|
152
|
+
validateNamespace(appConfig);
|
|
153
|
+
|
|
154
|
+
let ordDocument = createDefaultORDDocument(linkedCsn, appConfig);
|
|
155
|
+
const packageIds = extractPackageIds(ordDocument);
|
|
156
|
+
// TODO: add testcase without apiResources or event, no empty package
|
|
157
|
+
ordDocument.apiResources = _getAPIResources(linkedCsn, appConfig, packageIds);
|
|
158
|
+
const eventResources = _getEventResources(linkedCsn, appConfig, packageIds);
|
|
159
|
+
if (eventResources.length) {
|
|
160
|
+
ordDocument.eventResources = eventResources;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
ordDocument = extendCustomORDContentIfExists(appConfig, ordDocument, logger);
|
|
164
|
+
|
|
165
|
+
return ordDocument;
|
|
166
|
+
};
|
package/lib/templates.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
const defaults = require("./defaults");
|
|
2
2
|
const cds = require("@sap/cds");
|
|
3
|
-
const
|
|
4
|
-
|
|
3
|
+
const _ = require("lodash");
|
|
4
|
+
const { ORD_EXTENSIONS_PREFIX } = require("./constants");
|
|
5
|
+
|
|
6
|
+
function unflatten(flattedObject) {
|
|
7
|
+
let result = {}
|
|
8
|
+
_.keys(flattedObject).forEach(function (key) {
|
|
9
|
+
_.set(result, key, flattedObject[key])
|
|
10
|
+
})
|
|
11
|
+
return result
|
|
5
12
|
}
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
(ordExtensions, [key, value]) => ({
|
|
18
|
-
...ordExtensions,
|
|
19
|
-
[key.slice("@ORD.Extensions.".length)]: value,
|
|
20
|
-
}),
|
|
21
|
-
{}
|
|
22
|
-
);
|
|
14
|
+
function readORDExtensions(model) {
|
|
15
|
+
const ordExtensions = {};
|
|
16
|
+
for (const key in model) {
|
|
17
|
+
if (key.startsWith(ORD_EXTENSIONS_PREFIX)) {
|
|
18
|
+
const ordKey = key.replace(ORD_EXTENSIONS_PREFIX, "");
|
|
19
|
+
ordExtensions[ordKey] = model[key];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return unflatten(ordExtensions);
|
|
23
|
+
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Reads the service definition and returns an array of entryPoint paths.
|
|
@@ -29,28 +30,28 @@ const fReadORDExtensions = (srv) =>
|
|
|
29
30
|
* @returns {Array} An array containing paths and it's kind.
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const _generatePaths = (srv, srvDefinition) => {
|
|
34
|
+
const srvObj = { name: srv, definition: srvDefinition };
|
|
35
|
+
const protocols = cds.service.protocols;
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
const paths = protocols.endpoints4(srvObj);
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
//TODO: check graphql replication in paths object and re-visit logic
|
|
40
|
+
//removing instances of graphql protocol from paths
|
|
41
|
+
for (var index = paths.length - 1; index >= 0; index--) {
|
|
42
|
+
if (paths[index].kind === "graphql") {
|
|
43
|
+
console.warn("Graphql protocol is not supported.");
|
|
44
|
+
paths.splice(index, 1);
|
|
45
|
+
}
|
|
44
46
|
}
|
|
45
|
-
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
//putting default as odata in case no supported protcol is there
|
|
49
|
+
if (paths.length === 0) {
|
|
50
|
+
srvDefinition["@odata"] = true;
|
|
51
|
+
paths.push({ kind: "odata", path: protocols.path4(srvDefinition) });
|
|
52
|
+
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
return paths;
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
/**
|
|
@@ -59,30 +60,21 @@ const fGeneratePaths = (srv, srvDefinition) => {
|
|
|
59
60
|
* @param {string} entity The name of the entity.
|
|
60
61
|
* @returns {Object} An object for the entity type.
|
|
61
62
|
*/
|
|
62
|
-
const
|
|
63
|
-
|
|
63
|
+
const createEntityTypeTemplate = (entity) => ({
|
|
64
|
+
ordId: `sap.odm:entityType:${entity["@ODM.entityName"]}:v1`,
|
|
64
65
|
});
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*/
|
|
73
|
-
function _getGroupID(fullyQualifiedName, groupTypeId = defaults.groupTypeId, isService = true) {
|
|
74
|
-
if (isService) {
|
|
75
|
-
return `${groupTypeId}:${fReplaceSpecialCharacters(global.namespace)}:${fullyQualifiedName}`;
|
|
76
|
-
} else {
|
|
77
|
-
return `${groupTypeId}:${fReplaceSpecialCharacters(global.namespace)}:` +
|
|
78
|
-
`${fullyQualifiedName.slice(0, fullyQualifiedName.lastIndexOf("."))}`;
|
|
79
|
-
}
|
|
67
|
+
function _getGroupID(
|
|
68
|
+
fullyQualifiedName,
|
|
69
|
+
groupTypeId = defaults.groupTypeId,
|
|
70
|
+
appConfig,
|
|
71
|
+
) {
|
|
72
|
+
return `${groupTypeId}:${appConfig.ordNamespace}:${fullyQualifiedName}`;
|
|
80
73
|
}
|
|
81
74
|
|
|
82
|
-
|
|
83
75
|
/**
|
|
84
76
|
* This is a function to resolve the title of the service group.
|
|
85
|
-
*
|
|
77
|
+
*
|
|
86
78
|
* @param {string} srv The name of the service.
|
|
87
79
|
* @returns {string} The title of the service group.
|
|
88
80
|
*/
|
|
@@ -90,265 +82,146 @@ function _getTitleFromServiceName(srv) {
|
|
|
90
82
|
let serviceName = srv.substring(srv.lastIndexOf(".") + 1);
|
|
91
83
|
let index = serviceName.indexOf("Service");
|
|
92
84
|
if (index >= 0) {
|
|
93
|
-
return `${serviceName.substring(0, index)} Service
|
|
85
|
+
return `${serviceName.substring(0, index)} Service`;
|
|
94
86
|
} else {
|
|
95
|
-
return `${serviceName} Service
|
|
87
|
+
return `${serviceName} Service`;
|
|
96
88
|
}
|
|
97
89
|
}
|
|
98
90
|
|
|
99
|
-
|
|
100
91
|
/**
|
|
101
92
|
* This is a template function to create group object of a service for groups array in ORD doc.
|
|
102
|
-
*
|
|
103
|
-
* @param {string}
|
|
104
|
-
* @param {object}
|
|
93
|
+
*
|
|
94
|
+
* @param {string} serviceName The name of the service.
|
|
95
|
+
* @param {object} serviceDefinition The definition of the service
|
|
105
96
|
* @param {Set} groupIds A set of group ids.
|
|
106
97
|
* @returns {Object} A group object.
|
|
107
98
|
*/
|
|
108
|
-
const
|
|
109
|
-
const ordExtensions =
|
|
99
|
+
const createGroupsTemplateForService = (serviceName, serviceDefinition, appConfig) => {
|
|
100
|
+
const ordExtensions = readORDExtensions(serviceDefinition);
|
|
110
101
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
102
|
+
if (!serviceDefinition) {
|
|
103
|
+
console.warn("Unable to find service definition:", serviceName)
|
|
104
|
+
return undefined
|
|
114
105
|
}
|
|
115
106
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
groupId: groupId,
|
|
124
|
-
groupTypeId: `${defaults.groupTypeId}`,
|
|
125
|
-
title: ordExtensions.title ?? _getTitleFromServiceName(srv)
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
*
|
|
133
|
-
* @param {string} event The name of the event.
|
|
134
|
-
* @returns {string} The title of the event group.
|
|
135
|
-
*/
|
|
136
|
-
function _getEventTitle(event) {
|
|
137
|
-
let serviceName = event.substring(0, event.lastIndexOf("."));
|
|
138
|
-
return _getTitleFromServiceName(serviceName);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* This is a template function to create group object of an event for groups array in ORD doc.
|
|
143
|
-
*
|
|
144
|
-
* @param {string} event The name of the event.
|
|
145
|
-
* @param {object} eventDefinition The definition of the event.
|
|
146
|
-
* @param {Set} groupIds A set of group ids.
|
|
147
|
-
* @returns {Object} A group object.
|
|
148
|
-
*/
|
|
149
|
-
const fCreateGroupsTemplateForEvent = (event, eventDefinition, groupIds) => {
|
|
150
|
-
const ordExtensions = fReadORDExtensions(eventDefinition);
|
|
151
|
-
|
|
152
|
-
let fullyQualifiedEventName = event;
|
|
153
|
-
if(!event.includes(global.capNamespace)){
|
|
154
|
-
fullyQualifiedEventName = global.capNamespace + "." + event;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let groupId = _getGroupID(fullyQualifiedEventName, defaults.groupTypeId, global, false);
|
|
158
|
-
if (groupIds.has(groupId)) {
|
|
159
|
-
return null;
|
|
160
|
-
} else {
|
|
161
|
-
groupIds.add(groupId);
|
|
162
|
-
return {
|
|
163
|
-
groupId: groupId,
|
|
164
|
-
groupTypeId: `${defaults.groupTypeId}`,
|
|
165
|
-
title: ordExtensions.title ?? _getEventTitle(event)
|
|
166
|
-
};
|
|
167
|
-
}
|
|
107
|
+
const groupId = _getGroupID(serviceName, defaults.groupTypeId, appConfig);
|
|
108
|
+
return {
|
|
109
|
+
groupId: groupId,
|
|
110
|
+
groupTypeId: defaults.groupTypeId,
|
|
111
|
+
title: ordExtensions.title ?? _getTitleFromServiceName(serviceName)
|
|
112
|
+
};
|
|
168
113
|
}
|
|
169
114
|
|
|
170
115
|
/**
|
|
171
116
|
* This is a template function to create API Resource object for API Resource Array.
|
|
172
117
|
*
|
|
173
|
-
* @param {string}
|
|
174
|
-
* @param {object}
|
|
118
|
+
* @param {string} serviceName The name of the service.
|
|
119
|
+
* @param {object} serviceDefinition The definition of the service
|
|
175
120
|
* @returns {Array} An array of objects for the API Resources.
|
|
176
121
|
*/
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
122
|
+
const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, packageIds) => {
|
|
123
|
+
const ordExtensions = readORDExtensions(serviceDefinition);
|
|
124
|
+
const paths = _generatePaths(serviceName, serviceDefinition);
|
|
125
|
+
const apiResources = [];
|
|
181
126
|
|
|
182
|
-
let srvName = srv;
|
|
183
|
-
if (!srvName.includes(global.capNamespace)) {
|
|
184
|
-
srvName = global.capNamespace + "." + srv;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (checkEntityFunctionAction(srvDefinition, global).length > 0) {
|
|
188
127
|
paths.forEach((generatedPath) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
128
|
+
let resourceDefinitions = [
|
|
129
|
+
{
|
|
130
|
+
type: "openapi-v3",
|
|
131
|
+
mediaType: "application/json",
|
|
132
|
+
url: `/.well-known/open-resource-discovery/v1/api-metadata/${serviceName}.oas3.json`,
|
|
133
|
+
accessStrategies: [{ type: "open" }],
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
if (generatedPath.kind !== "rest") {
|
|
138
|
+
//edmx resource definition is not generated in case of 'rest' protocol
|
|
139
|
+
resourceDefinitions.push({
|
|
140
|
+
type: "edmx",
|
|
141
|
+
mediaType: "application/xml",
|
|
142
|
+
url: `/.well-known/open-resource-discovery/v1/api-metadata/${serviceName}.edmx`,
|
|
143
|
+
accessStrategies: [{ type: "open" }],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let obj = {
|
|
148
|
+
ordId: `${appConfig.ordNamespace}:apiResource:${serviceName}:v1`,
|
|
149
|
+
title:
|
|
150
|
+
serviceDefinition["@title"] ??
|
|
151
|
+
serviceDefinition["@Common.Label"] ??
|
|
152
|
+
serviceName,
|
|
153
|
+
shortDescription: serviceName,
|
|
154
|
+
description:
|
|
155
|
+
serviceDefinition["@Core.Description"] ??
|
|
156
|
+
serviceName,
|
|
157
|
+
version: "1.0.0",
|
|
158
|
+
visibility: "public",
|
|
159
|
+
partOfPackage: _getPackageID(appConfig.ordNamespace, packageIds, "api"),
|
|
160
|
+
partOfGroups: [_getGroupID(serviceName, defaults.groupTypeId, appConfig)],
|
|
161
|
+
releaseStatus: "active",
|
|
162
|
+
apiProtocol:
|
|
163
|
+
generatedPath.kind === "odata" ? "odata-v4" : generatedPath.kind,
|
|
164
|
+
resourceDefinitions: resourceDefinitions,
|
|
165
|
+
entryPoints: [generatedPath.path],
|
|
166
|
+
extensible: {
|
|
167
|
+
supported: "no",
|
|
168
|
+
},
|
|
169
|
+
entityTypeMappings: [{ entityTypeTargets: appConfig.odmEntity }],
|
|
170
|
+
|
|
171
|
+
...ordExtensions,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
apiResources.push(obj);
|
|
175
|
+
});
|
|
197
176
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
resourceDefinitions.push({
|
|
201
|
-
type: "edmx",
|
|
202
|
-
mediaType: "application/xml",
|
|
203
|
-
url: `/.well-known/open-resource-discovery/v1/api-metadata/${srv}.edmx`,
|
|
204
|
-
accessStrategies: [{ type: "open" }],
|
|
205
|
-
});
|
|
206
|
-
}
|
|
177
|
+
if (apiResources.length > 0) return apiResources;
|
|
178
|
+
};
|
|
207
179
|
|
|
208
|
-
|
|
209
|
-
|
|
180
|
+
const createEventResourceTemplate = (serviceName, eventDefinition, appConfig, packageIds) => {
|
|
181
|
+
const ordExtensions = readORDExtensions(eventDefinition);
|
|
182
|
+
return {
|
|
183
|
+
ordId: `${appConfig.ordNamespace}:eventResource:${serviceName}:v1`,
|
|
210
184
|
title:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
shortDescription:
|
|
216
|
-
ordExtensions.shortDescription ??
|
|
217
|
-
`Here we have the shortDescription for ${srv}`,
|
|
185
|
+
eventDefinition["@title"] ??
|
|
186
|
+
eventDefinition["@Common.Label"] ??
|
|
187
|
+
`ODM ${appConfig.appName.replace(/[^a-zA-Z0-9]/g, "")} Events`,
|
|
188
|
+
shortDescription: `${serviceName} event resource`,
|
|
218
189
|
description:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
190
|
+
eventDefinition['@description'] ?? eventDefinition['@Core.Description'] ??
|
|
191
|
+
"CAP Event resource describing events / messages.",
|
|
192
|
+
version: "1.0.0",
|
|
193
|
+
releaseStatus: "active",
|
|
194
|
+
partOfPackage: _getPackageID(appConfig.ordNamespace, packageIds, 'event'),
|
|
195
|
+
partOfGroups: [_getGroupID(serviceName, defaults.groupTypeId, appConfig)],
|
|
196
|
+
visibility: "public",
|
|
197
|
+
resourceDefinitions: [
|
|
198
|
+
{
|
|
199
|
+
type: "asyncapi-v2",
|
|
200
|
+
mediaType: "application/json",
|
|
201
|
+
url: `/.well-known/open-resource-discovery/v1/api-metadata/${serviceName}.asyncapi2.json`,
|
|
202
|
+
accessStrategies: [
|
|
203
|
+
{
|
|
204
|
+
type: "open",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
231
208
|
],
|
|
232
|
-
|
|
233
|
-
generatedPath.kind === "odata" ? "odata-v4" : generatedPath.kind,
|
|
234
|
-
resourceDefinitions: resourceDefinitions,
|
|
235
|
-
entryPoints: [generatedPath.path],
|
|
236
|
-
extensible: {
|
|
237
|
-
supported: ordExtensions["extensible.supported"] ?? "no",
|
|
238
|
-
},
|
|
239
|
-
entityTypeMappings: [{ entityTypeTargets: global.aODMEntity }],
|
|
240
|
-
};
|
|
209
|
+
extensible: { supported: "no" },
|
|
241
210
|
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (apiResources.length > 0) return apiResources;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* This is a template function to create Event Resource object for Event Resource Array.
|
|
250
|
-
*
|
|
251
|
-
* @param {string} srv The name of the event.
|
|
252
|
-
* @param {object} srvDefinition The definition of the event.
|
|
253
|
-
* @returns {Object} An object for the Event Resource.
|
|
254
|
-
*/
|
|
255
|
-
const fCreateEventResourceTemplate = (srv, srvDefinition, global,packageIds) => {
|
|
256
|
-
const ordExtensions = fReadORDExtensions(srvDefinition);
|
|
257
|
-
let srvName = srv;
|
|
258
|
-
if(!srvName.includes(global.capNamespace)){
|
|
259
|
-
srvName = global.capNamespace + "." + srv;
|
|
260
|
-
}
|
|
261
|
-
return {
|
|
262
|
-
ordId: `${fReplaceSpecialCharacters(global.namespace)}:eventResource:${srvName}:v1`,
|
|
263
|
-
title: ordExtensions.title ?? srvDefinition['@title'] ?? srvDefinition['@Common.Label'] ?? `ODM ${global.appName.replace(/[^a-zA-Z0-9]/g, '')} Events`,
|
|
264
|
-
shortDescription: ordExtensions.shortDescription ?? "Example ODM Event",
|
|
265
|
-
description: ordExtensions.description ??
|
|
266
|
-
srvDefinition['@description'] ?? srvDefinition['@Core.Description'] ??
|
|
267
|
-
`This is an example event catalog that contains only a partial ODM ${global.appName} V1 event`,
|
|
268
|
-
version: ordExtensions.version ?? "1.0.0",
|
|
269
|
-
releaseStatus: ordExtensions.releaseStatus ?? "beta",
|
|
270
|
-
partOfPackage: _getPackageID(global.capNamespace,packageIds,'event'),
|
|
271
|
-
partOfGroups: [_getGroupID(srvName, defaults.groupTypeId, global, false)],
|
|
272
|
-
visibility: ordExtensions.visibility ?? "public",
|
|
273
|
-
resourceDefinitions: [
|
|
274
|
-
{
|
|
275
|
-
type: "asyncapi-v2",
|
|
276
|
-
mediaType: "application/json",
|
|
277
|
-
url: `/.well-known/open-resource-discovery/v1/api-metadata/${srvDefinition._service.name}.asyncapi2.json`,
|
|
278
|
-
accessStrategies: [
|
|
279
|
-
{
|
|
280
|
-
type: "open",
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
extensible: { supported: ordExtensions["extensible.supported"] ?? "no" },
|
|
286
|
-
};
|
|
211
|
+
...ordExtensions
|
|
212
|
+
};
|
|
287
213
|
};
|
|
288
214
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
*/
|
|
295
|
-
function checkEntityFunctionAction(srvDefinition, global) {
|
|
296
|
-
if (srvDefinition.entities) {
|
|
297
|
-
return srvDefinition.entities.map((entity) => {
|
|
298
|
-
return {
|
|
299
|
-
type: "entity",
|
|
300
|
-
name: entity.name,
|
|
301
|
-
entityType: entity.name,
|
|
302
|
-
entitySet: entity.name,
|
|
303
|
-
entityTypeMapping: `${global.namespace}:entityType:${entity.name}:v1`,
|
|
304
|
-
entitySetMapping: `${global.namespace}:entitySet:${entity.name}:v1`,
|
|
305
|
-
};
|
|
306
|
-
});
|
|
307
|
-
} else if (srvDefinition.actions) {
|
|
308
|
-
return srvDefinition.actions.map((action) => {
|
|
309
|
-
return {
|
|
310
|
-
type: "action",
|
|
311
|
-
name: action.name,
|
|
312
|
-
actionType: action.name,
|
|
313
|
-
actionMapping: `${global.namespace}:action:${action.name}:v1`,
|
|
314
|
-
};
|
|
315
|
-
});
|
|
316
|
-
} else if (srvDefinition.functions) {
|
|
317
|
-
return srvDefinition.functions.map((func) => {
|
|
318
|
-
return {
|
|
319
|
-
type: "function",
|
|
320
|
-
name: func.name,
|
|
321
|
-
functionType: func.name,
|
|
322
|
-
functionMapping: `${global.namespace}:function:${func.name}:v1`,
|
|
323
|
-
};
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* This is a function to get the corresponding package ordId mapped to the service.
|
|
330
|
-
*/
|
|
331
|
-
|
|
332
|
-
function _getPackageID(capNamespace, packageIds, apiOrEvent) {
|
|
333
|
-
if (packageIds instanceof Set) {
|
|
334
|
-
const packageArray = Array.from(packageIds);
|
|
335
|
-
|
|
336
|
-
if (apiOrEvent === "api") {
|
|
337
|
-
const apiPackage = packageArray.find(pkg => pkg.includes("-api"));
|
|
338
|
-
if (apiPackage) return apiPackage;
|
|
339
|
-
} else if (apiOrEvent === "event") {
|
|
340
|
-
const eventPackage = packageArray.find(pkg => pkg.includes("-event"));
|
|
341
|
-
if (eventPackage) return eventPackage;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return packageArray.find(pkg => pkg.includes(capNamespace));
|
|
345
|
-
}
|
|
215
|
+
function _getPackageID(namespace, packageIds, resourceType) {
|
|
216
|
+
//TODO: without this check, it will be failed in ordPackageJson.test.js, need to check why it is failing
|
|
217
|
+
if (!(packageIds instanceof Set)) return;
|
|
218
|
+
const packageIdsArray = Array.from(packageIds);
|
|
219
|
+
return packageIdsArray.find((pkg) => pkg.includes("-" + resourceType)) || packageIdsArray.find((pkg) => pkg.includes(namespace));
|
|
346
220
|
}
|
|
347
221
|
|
|
348
222
|
module.exports = {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
fCreateEventResourceTemplate,
|
|
223
|
+
createEntityTypeTemplate,
|
|
224
|
+
createGroupsTemplateForService,
|
|
225
|
+
createAPIResourceTemplate,
|
|
226
|
+
createEventResourceTemplate,
|
|
354
227
|
};
|
package/package.json
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
2
|
+
"name": "@cap-js/ord",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "CAP Plugin for generating ORD document.",
|
|
5
|
+
"repository": "cap-js/ord",
|
|
6
|
+
"author": "SAP SE (https://www.sap.com)",
|
|
7
|
+
"homepage": "https://cap.cloud.sap/",
|
|
8
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
9
|
+
"main": "cds-plugin.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"lib",
|
|
12
|
+
"data",
|
|
13
|
+
"LICENSES"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"lint": "npx eslint .",
|
|
17
|
+
"test": "jest --ci --collectCoverage",
|
|
18
|
+
"update-snapshot": "jest --ci --updateSnapshot",
|
|
19
|
+
"cds:version": "cds v -i"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"eslint": "^8",
|
|
23
|
+
"jest": "^29.7.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@cap-js/asyncapi": "^1.0.0",
|
|
27
|
+
"@cap-js/openapi": "^1.0.2",
|
|
28
|
+
"@sap/cds": "^8.1.1",
|
|
29
|
+
"@sap/cds-compiler": "5.0.6"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"lodash": "^4.17.21"
|
|
33
|
+
}
|
|
34
|
+
}
|