@cap-js/ord 1.6.0 → 1.8.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/lib/build.js CHANGED
@@ -1,13 +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
10
  const registerCompileTargets = require("./common/register-compile-targets");
10
- const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME } = require("./constants");
11
+ const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME, DOCUMENT_PERSPECTIVES } = require("./constants");
11
12
 
12
13
  module.exports = class OrdBuildPlugin extends cds.build.Plugin {
13
14
  static taskDefaults = { src: cds.env.folders.srv };
@@ -53,7 +54,7 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
53
54
  resourceDefinition.url = this._createRelativePath(resourceDefinition.url);
54
55
  });
55
56
 
56
- return clone;
57
+ return defaults.adjustForPerspective(clone, DOCUMENT_PERSPECTIVES.SystemVersion);
57
58
  }
58
59
 
59
60
  _createRelativePath(url) {
@@ -12,7 +12,7 @@ module.exports = () => {
12
12
  // registrations. Re-apply so that compilation works accordingly during build and runtime.
13
13
 
14
14
  Object.keys(cds.env.plugins || {})
15
- .filter((plugin) => plugin !== "@cap-js/ord")
15
+ .filter((plugin) => !!PROTOCOL_PROVIDERS[plugin])
16
16
  .forEach((plugin) => {
17
17
  try {
18
18
  const protocol = PROTOCOL_PROVIDERS[plugin];
@@ -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,69 +89,88 @@ 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
- consumptionBundles: (appConfig) => [
127
- {
128
- ordId: `${regexWithRemoval(appConfig.appName)}:consumptionBundle:noAuth:v1`,
129
- version: "1.0.0",
130
- lastUpdate: appConfig.lastUpdate,
131
- title: "Unprotected resources",
132
- shortDescription: "If we have another protected API then it will be another object",
133
- description:
134
- "This Consumption Bundle contains all resources of the reference app which are unprotected and do not require authentication",
135
- },
136
- ],
137
-
138
- apiResources: [],
139
- eventResources: [],
140
- entityTypes: [],
141
- baseTemplate: (authConfig) => {
125
+ baseTemplate: (authConfig, tenant) => {
142
126
  // Get access strategies from the provided authConfig
143
127
  // If auth config is not available, fall back to empty array
128
+ const toggles = cds.env.requires.toggles;
129
+ const extensibility = cds.env.requires.extensibility;
144
130
  const accessStrategies = authConfig?.accessStrategies || [];
131
+
145
132
  return {
146
133
  openResourceDiscoveryV1: {
147
134
  documents: [
148
135
  {
149
136
  url: "/ord/v1/documents/ord-document",
137
+ perspective: DOCUMENT_PERSPECTIVES.SystemVersion,
150
138
  accessStrategies,
151
139
  },
140
+ ...(!tenant || !(toggles || extensibility)
141
+ ? []
142
+ : [
143
+ {
144
+ url: `/ord/v1/documents/ord-document?perspective=${encodeURIComponent(DOCUMENT_PERSPECTIVES.SystemInstance)}`,
145
+ perspective: DOCUMENT_PERSPECTIVES.SystemInstance,
146
+ accessStrategies,
147
+ },
148
+ ]),
152
149
  ],
153
150
  },
154
151
  };
155
152
  },
153
+ adjustForPerspective: (document, perspective) => {
154
+ document.perspective = perspective;
155
+
156
+ if (perspective === DOCUMENT_PERSPECTIVES.SystemVersion) {
157
+ document.describedSystemVersion = cds.env["ord"]?.describedSystemVersion ?? {
158
+ version: JSON.parse(fs.readFileSync(join(cds.root, "package.json"), "utf-8")).version,
159
+ };
160
+ } else if (perspective === DOCUMENT_PERSPECTIVES.SystemInstance) {
161
+ (document.apiResources || []).forEach((apiResource) => {
162
+ (apiResource.resourceDefinitions || []).forEach((apiResourceDefinition) => {
163
+ apiResourceDefinition.url += `?perspective=${encodeURIComponent(perspective)}`;
164
+ });
165
+ });
166
+
167
+ (document.eventResources || []).forEach((eventResource) => {
168
+ (eventResource.resourceDefinitions || []).forEach((eventResourceDefinition) => {
169
+ eventResourceDefinition.url += `?perspective=${encodeURIComponent(perspective)}`;
170
+ });
171
+ });
172
+ }
173
+
174
+ return document;
175
+ },
156
176
  };
@@ -89,11 +89,12 @@ function createIntegrationDependency(externalServices, appConfig, packageIds) {
89
89
 
90
90
  // Read IntegrationDependency level config from cdsrc
91
91
  const integrationDepConfig = appConfig.env?.integrationDependency || {};
92
+ const version = integrationDepConfig.version || "1.0.0";
92
93
 
93
94
  return {
94
- ordId: `${appConfig.ordNamespace}:${ORD_RESOURCE_TYPE.integrationDependency}:${INTEGRATION_DEPENDENCY_RESOURCE_NAME}:v1`,
95
+ ordId: `${appConfig.ordNamespace}:${ORD_RESOURCE_TYPE.integrationDependency}:${INTEGRATION_DEPENDENCY_RESOURCE_NAME}:v${version.split(".")[0]}`,
95
96
  title: "External Dependencies",
96
- version: "1.0.0",
97
+ version: version,
97
98
  releaseStatus: "active",
98
99
  visibility: RESOURCE_VISIBILITY.public,
99
100
  mandatory: false,
@@ -122,6 +123,4 @@ module.exports = {
122
123
  // Exported for testing
123
124
  collectExternalServices,
124
125
  createIntegrationDependency,
125
- parseDataProductOrdId,
126
- isExternalDataProduct,
127
126
  };
@@ -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
  }