@cap-js/ord 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,40 +89,38 @@ module.exports = {
88
89
  vendor: "customer:vendor:Customer:",
89
90
  },
90
91
  ],
91
- groupTypeId: "sap.cds:service",
92
- packages: function getPackageData(appConfig) {
92
+ packages: (appConfig, products) => {
93
93
  const name = appConfig.appName;
94
94
  const ordNamespace = appConfig.ordNamespace;
95
- const productsOrdId = appConfig.existingProductORDId || appConfig.products?.[0]?.ordId;
95
+ const productsOrdId = appConfig.existingProductORDId || products?.[0]?.ordId;
96
96
  const { vendor, ...envValues } = mapEnvPackageInfo(appConfig?.env?.packages?.[0]);
97
- if (hasSAPPolicyLevel(appConfig.policyLevels)) {
98
- return _.uniqBy(
99
- packageTypes.flatMap(({ tag, type }) =>
100
- Object.values(RESOURCE_VISIBILITY).map((visibility) => ({
101
- ordId: generateUniquePackageOrdId(ordNamespace, name, tag, visibility),
102
- title: nameWithSpaces(name),
103
- ...generatePackageDescriptions(name, type, visibility),
104
- version: "1.0.0",
105
- ...(productsOrdId && { partOfProducts: [productsOrdId || defaultProductOrdId(name)] }),
106
- vendor: vendor || "customer:vendor:Customer:",
107
- ...envValues,
108
- })),
109
- ),
110
- "ordId",
111
- );
112
- } else {
113
- return [
114
- {
115
- ordId: generateUniquePackageOrdId(ordNamespace, name, "", RESOURCE_VISIBILITY.public),
97
+ const visibilities = !appConfig.hasSAPPolicyLevel
98
+ ? [RESOURCE_VISIBILITY.public]
99
+ : Object.values(RESOURCE_VISIBILITY);
100
+ const packageTypes = !appConfig.hasSAPPolicyLevel
101
+ ? [{ tag: "", type: "General" }]
102
+ : [
103
+ { tag: `-${ORD_RESOURCE_TYPE.api}`, type: "APIs" },
104
+ { tag: `-${ORD_RESOURCE_TYPE.event}`, type: "Events" },
105
+ { tag: `-${ORD_RESOURCE_TYPE.entityType}`, type: "Entity Types" },
106
+ { tag: `-${ORD_RESOURCE_TYPE.dataProduct}`, type: "Data Products" },
107
+ { tag: `-${ORD_RESOURCE_TYPE.integrationDependency}`, type: "Integration Dependencies" },
108
+ ];
109
+
110
+ return _.uniqBy(
111
+ packageTypes.flatMap(({ tag, type }) =>
112
+ visibilities.map((visibility) => ({
113
+ ordId: generateUniquePackageOrdId(ordNamespace, name, tag, visibility),
116
114
  title: nameWithSpaces(name),
117
- ...generatePackageDescriptions(name, "General", RESOURCE_VISIBILITY.public),
115
+ ...generatePackageDescriptions(name, type, visibility),
118
116
  version: "1.0.0",
119
117
  ...(productsOrdId && { partOfProducts: [productsOrdId || defaultProductOrdId(name)] }),
120
118
  vendor: vendor || "customer:vendor:Customer:",
121
119
  ...envValues,
122
- },
123
- ];
124
- }
120
+ })),
121
+ ),
122
+ CONTENT_MERGE_KEY,
123
+ );
125
124
  },
126
125
  consumptionBundles: (appConfig) => [
127
126
  {
@@ -134,23 +133,55 @@ module.exports = {
134
133
  "This Consumption Bundle contains all resources of the reference app which are unprotected and do not require authentication",
135
134
  },
136
135
  ],
137
-
138
- apiResources: [],
139
- eventResources: [],
140
- entityTypes: [],
141
- baseTemplate: (authConfig) => {
136
+ baseTemplate: (authConfig, tenant) => {
142
137
  // Get access strategies from the provided authConfig
143
138
  // If auth config is not available, fall back to empty array
139
+ const toggles = cds.env.requires.toggles;
140
+ const extensibility = cds.env.requires.extensibility;
144
141
  const accessStrategies = authConfig?.accessStrategies || [];
142
+
145
143
  return {
146
144
  openResourceDiscoveryV1: {
147
145
  documents: [
148
146
  {
149
147
  url: "/ord/v1/documents/ord-document",
148
+ perspective: DOCUMENT_PERSPECTIVES.SystemVersion,
150
149
  accessStrategies,
151
150
  },
151
+ ...(!tenant || !(toggles || extensibility)
152
+ ? []
153
+ : [
154
+ {
155
+ url: `/ord/v1/documents/ord-document?perspective=${encodeURIComponent(DOCUMENT_PERSPECTIVES.SystemInstance)}`,
156
+ perspective: DOCUMENT_PERSPECTIVES.SystemInstance,
157
+ accessStrategies,
158
+ },
159
+ ]),
152
160
  ],
153
161
  },
154
162
  };
155
163
  },
164
+ adjustForPerspective: (document, perspective) => {
165
+ document.perspective = perspective;
166
+
167
+ if (perspective === DOCUMENT_PERSPECTIVES.SystemVersion) {
168
+ document.describedSystemVersion = cds.env["ord"]?.describedSystemVersion ?? {
169
+ version: JSON.parse(fs.readFileSync(join(cds.root, "package.json"), "utf-8")).version,
170
+ };
171
+ } else if (perspective === DOCUMENT_PERSPECTIVES.SystemInstance) {
172
+ (document.apiResources || []).forEach((apiResource) => {
173
+ (apiResource.resourceDefinitions || []).forEach((apiResourceDefinition) => {
174
+ apiResourceDefinition.url += `?perspective=${encodeURIComponent(perspective)}`;
175
+ });
176
+ });
177
+
178
+ (document.eventResources || []).forEach((eventResource) => {
179
+ (eventResource.resourceDefinitions || []).forEach((eventResourceDefinition) => {
180
+ eventResourceDefinition.url += `?perspective=${encodeURIComponent(perspective)}`;
181
+ });
182
+ });
183
+ }
184
+
185
+ return document;
186
+ },
156
187
  };
@@ -122,6 +122,4 @@ module.exports = {
122
122
  // Exported for testing
123
123
  collectExternalServices,
124
124
  createIntegrationDependency,
125
- parseDataProductOrdId,
126
- isExternalDataProduct,
127
125
  };
@@ -54,7 +54,7 @@ function add_i18n_texts(csn) {
54
54
  if (n.startsWith("@")) {
55
55
  let annoVal = o[n];
56
56
  if (typeof annoVal === "string" && annoVal.startsWith("{i18n>")) {
57
- let [, x] = annoVal.match(/^\{i18n>(.*)\}$/) || [];
57
+ let [, x] = annoVal.match(/^\{i18n>(.*)}$/) || [];
58
58
  if (x) keys.add(x);
59
59
  }
60
60
  }
package/lib/ord.js CHANGED
@@ -1,221 +1,22 @@
1
+ const _ = require("lodash");
2
+ const cds = require("@sap/cds");
3
+
4
+ const Logger = require("./logger");
5
+ const defaults = require("./defaults");
6
+ const { createAuthConfig } = require("./auth/authentication");
7
+ const Configuration = require("./configuration");
8
+ const { getIntegrationDependencies } = require("./integration-dependency");
1
9
  const {
2
- BLOCKED_SERVICE_NAME,
3
- CDS_ELEMENT_KIND,
4
- CONTENT_MERGE_KEY,
5
- ENTITY_RELATIONSHIP_ANNOTATION,
6
- ORD_ODM_ENTITY_NAME_ANNOTATION,
7
- } = require("./constants");
10
+ getCustomORDContent,
11
+ compareAndHandleCustomORDContentWithExistingContent,
12
+ } = require("./extend-ord-with-custom");
8
13
  const {
9
14
  createAPIResourceTemplate,
10
15
  createEntityTypeTemplate,
11
- createEntityTypeMappingsItemTemplate,
12
16
  createEventResourceTemplate,
13
17
  createGroupsTemplateForService,
14
18
  _propagateORDVisibility,
15
19
  } = require("./templates");
16
- const { getIntegrationDependencies } = require("./integration-dependency");
17
- const {
18
- getCustomORDContent,
19
- compareAndHandleCustomORDContentWithExistingContent,
20
- } = require("./extend-ord-with-custom");
21
- const { getRFC3339Date } = require("./date");
22
- const { createAuthConfig } = require("./auth/authentication");
23
-
24
- const Logger = require("./logger");
25
- const _ = require("lodash");
26
- const cds = require("@sap/cds");
27
- const defaults = require("./defaults");
28
- const path = require("path");
29
-
30
- const initializeAppConfig = (csn) => {
31
- const packageJson = _loadPackageJson();
32
- const packageName = packageJson.name;
33
- const appName = _formatAppName(packageName);
34
- const lastUpdate = getRFC3339Date();
35
-
36
- const ordNamespace = _getORDNamespace(packageName);
37
- const eventApplicationNamespace = cds.env?.export?.asyncapi?.applicationNamespace;
38
-
39
- _validateNamespaces(ordNamespace, eventApplicationNamespace);
40
-
41
- const { serviceNames, apiResourceNames, apiEndpoints, eventServiceNames, entityTypeTargets } =
42
- _triageCsnDefinitions(csn);
43
-
44
- return {
45
- env: cds.env["ord"],
46
- lastUpdate,
47
- appName,
48
- apiEndpoints: Array.from(apiEndpoints),
49
- eventServiceNames,
50
- serviceNames,
51
- apiResourceNames,
52
- entityTypeTargets: _.uniqBy(entityTypeTargets, CONTENT_MERGE_KEY),
53
- ordNamespace,
54
- eventApplicationNamespace,
55
- packageName,
56
- };
57
- };
58
-
59
- function _loadPackageJson() {
60
- const packageJsonPath = path.join(cds.root, "package.json");
61
- if (!cds.utils.exists(packageJsonPath)) {
62
- throw new Error(`package.json not found in the project root directory`);
63
- }
64
- return require(packageJsonPath);
65
- }
66
-
67
- function _formatAppName(packageName) {
68
- return packageName.replace(/^[@]/, "").replace(/[@/]/g, "-");
69
- }
70
-
71
- function _getORDNamespace(packageName) {
72
- const vendorNamespace = "customer";
73
- return cds.env["ord"]?.namespace || `${vendorNamespace}.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
74
- }
75
-
76
- function _validateNamespaces(ordNamespace, eventApplicationNamespace) {
77
- if (eventApplicationNamespace && ordNamespace !== eventApplicationNamespace) {
78
- Logger.warn("ORD and AsyncAPI namespaces should be the same.");
79
- }
80
- }
81
-
82
- function _triageCsnDefinitions(csn) {
83
- const pendingApiResourceNames = [];
84
- const apiEndpoints = new Set();
85
- const pendingEventServiceNames = new Set();
86
- const entityTypeTargets = [];
87
- const serviceNames = Object.keys(csn.definitions).filter((key) => _isValidService(key, csn.definitions[key]));
88
-
89
- for (const definitionKey of Object.keys(csn.definitions)) {
90
- const definitionObj = csn.definitions[definitionKey];
91
- if (
92
- definitionKey.includes(BLOCKED_SERVICE_NAME.MTXServices) ||
93
- definitionKey.includes(BLOCKED_SERVICE_NAME.OpenResourceDiscoveryService)
94
- ) {
95
- Logger.warn("ORD service name", definitionKey, "is blocked.");
96
- continue;
97
- }
98
- switch (definitionObj.kind) {
99
- case CDS_ELEMENT_KIND.service: {
100
- const apiResourceName = _handleApiResource(definitionKey, definitionObj);
101
- if (apiResourceName) pendingApiResourceNames.push(apiResourceName);
102
- break;
103
- }
104
- case CDS_ELEMENT_KIND.entity: {
105
- const result = _handleEntity(definitionKey, definitionObj);
106
- if (result) {
107
- if (result.apiEndpoint) apiEndpoints.add(result.apiEndpoint);
108
- if (result.entityTypeTarget) entityTypeTargets.push(...result.entityTypeTarget);
109
- }
110
- break;
111
- }
112
- case CDS_ELEMENT_KIND.event: {
113
- const event = _handleEvent(serviceNames, definitionKey, definitionObj);
114
- if (event) pendingEventServiceNames.add(event);
115
- break;
116
- }
117
- case CDS_ELEMENT_KIND.action:
118
- case CDS_ELEMENT_KIND.function: {
119
- const apiEndpoint = _handleActionOrFunction(definitionKey, definitionObj);
120
- if (apiEndpoint) apiEndpoints.add(apiEndpoint);
121
- break;
122
- }
123
- }
124
- }
125
-
126
- return {
127
- serviceNames,
128
- apiResourceNames: pendingApiResourceNames,
129
- apiEndpoints: Array.from(apiEndpoints),
130
- eventServiceNames: [...pendingEventServiceNames],
131
- entityTypeTargets,
132
- };
133
- }
134
-
135
- function _handleApiResource(apiResourceName, serviceDefinition) {
136
- if (
137
- _shouldSkipIfServiceOnlyContainsEvents(serviceDefinition) ||
138
- !_isValidService(apiResourceName, serviceDefinition)
139
- ) {
140
- return null;
141
- }
142
- return apiResourceName;
143
- }
144
-
145
- function _shouldSkipIfServiceOnlyContainsEvents(serviceDefinition) {
146
- const isActionsNotContained = !serviceDefinition.actions || Object.keys(serviceDefinition.actions).length === 0;
147
- const isFunctionsNotContained =
148
- !serviceDefinition.functions || Object.keys(serviceDefinition.functions).length === 0;
149
- const isEntitiesNotContained = !serviceDefinition.entities || Object.keys(serviceDefinition.entities).length === 0;
150
- const isEventsContained = serviceDefinition.events && Object.keys(serviceDefinition.events).length > 0;
151
- if (isActionsNotContained && isFunctionsNotContained && isEntitiesNotContained && isEventsContained) {
152
- return true;
153
- }
154
- return false;
155
- }
156
-
157
- function _shouldNotSkipIfServiceProtocolIsNone(keyDefinition) {
158
- if (keyDefinition["_service"] && keyDefinition["_service"]["@protocol"] === "none") {
159
- return false;
160
- }
161
- return true;
162
- }
163
-
164
- function _isBlockedServiceName(key) {
165
- const blockedServices = [BLOCKED_SERVICE_NAME.MTXServices, BLOCKED_SERVICE_NAME.OpenResourceDiscoveryService];
166
- return blockedServices.some((blocked) => key.includes(blocked));
167
- }
168
-
169
- function _isValidService(key, definition) {
170
- const isExternalService = Object.keys(cds).includes("requires") ? Object.keys(cds.requires).includes(key) : false;
171
-
172
- return (
173
- definition.kind === CDS_ELEMENT_KIND.service &&
174
- !definition["@cds.external"] &&
175
- definition["@protocol"] !== "none" &&
176
- !isExternalService &&
177
- !_isBlockedServiceName(key)
178
- );
179
- }
180
-
181
- function _handleEntity(key, keyDefinition) {
182
- if (!key.includes(".texts") && _shouldNotSkipIfServiceProtocolIsNone(keyDefinition)) {
183
- const apiEndpoint = key;
184
- let entityTypeTarget = null;
185
- if (keyDefinition[ORD_ODM_ENTITY_NAME_ANNOTATION] || keyDefinition[ENTITY_RELATIONSHIP_ANNOTATION]) {
186
- const mapping = createEntityTypeMappingsItemTemplate(keyDefinition);
187
- if (Array.isArray(mapping)) entityTypeTarget = mapping;
188
- else if (mapping) entityTypeTarget = [mapping];
189
- }
190
- return { apiEndpoint, entityTypeTarget };
191
- }
192
- return null;
193
- }
194
-
195
- function _handleEvent(serviceNames, key, keyDefinition) {
196
- if (_shouldNotSkipIfServiceProtocolIsNone(keyDefinition)) {
197
- for (const serviceName of serviceNames) {
198
- if (key.startsWith(serviceName + ".")) {
199
- return serviceName;
200
- }
201
- }
202
- }
203
- return null;
204
- }
205
-
206
- function _handleActionOrFunction(key, keyDefinition) {
207
- if (_shouldNotSkipIfServiceProtocolIsNone(keyDefinition)) {
208
- return key;
209
- }
210
- return null;
211
- }
212
-
213
- const _getPolicyLevels = (appConfig) =>
214
- appConfig.env?.policyLevels ||
215
- (appConfig.env?.policyLevel && [appConfig.env?.policyLevel]) ||
216
- defaults.policyLevels;
217
-
218
- const _getDescription = (appConfig) => appConfig.env?.description || defaults.description;
219
20
 
220
21
  const _getGroups = (csn, appConfig) => {
221
22
  return appConfig.serviceNames
@@ -223,18 +24,8 @@ const _getGroups = (csn, appConfig) => {
223
24
  .filter((resource) => !!resource);
224
25
  };
225
26
 
226
- const _getPackages = (appConfig) => {
227
- return defaults.packages(appConfig);
228
- };
229
-
230
- const _getEntityTypes = (appConfig, packageIds) => {
231
- if (!appConfig.entityTypeTargets?.length) return [];
232
-
233
- return appConfig.entityTypeTargets.flatMap((entity) => createEntityTypeTemplate(appConfig, packageIds, entity));
234
- };
235
-
236
27
  const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
237
- const apiResources = appConfig.apiResourceNames.flatMap((apiResourceName) =>
28
+ return appConfig.apiResourceNames.flatMap((apiResourceName) =>
238
29
  createAPIResourceTemplate(
239
30
  apiResourceName,
240
31
  csn.definitions[apiResourceName],
@@ -243,8 +34,6 @@ const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
243
34
  accessStrategies,
244
35
  ),
245
36
  );
246
-
247
- return apiResources;
248
37
  };
249
38
 
250
39
  const _getEventResources = (csn, appConfig, packageIds, accessStrategies) => {
@@ -253,16 +42,9 @@ const _getEventResources = (csn, appConfig, packageIds, accessStrategies) => {
253
42
  );
254
43
  };
255
44
 
256
- function _getOpenResourceDiscovery(appConfig) {
257
- return appConfig.env?.openResourceDiscovery || defaults.openResourceDiscovery;
258
- }
259
-
260
- function _getConsumptionBundles(appConfig) {
261
- return appConfig.env?.consumptionBundles || defaults.consumptionBundles(appConfig);
262
- }
263
-
264
45
  const _getProducts = (appConfig) => {
265
46
  const productsObj = defaults.products(appConfig.packageName);
47
+
266
48
  if (appConfig.env?.products) {
267
49
  const customProducts = appConfig.env.products[0];
268
50
  if (customProducts?.ordId?.toLowerCase().startsWith("sap")) {
@@ -273,40 +55,39 @@ const _getProducts = (appConfig) => {
273
55
  _.assign(productsObj[0], customProducts);
274
56
  }
275
57
  }
276
- appConfig.products = productsObj;
58
+
277
59
  return productsObj;
278
60
  };
279
61
 
280
- function createDefaultORDDocument(linkedCsn, appConfig) {
281
- appConfig.policyLevels = _getPolicyLevels(appConfig);
282
- let ordDocument = {
62
+ const _createDefaultORDDocument = (linkedCsn, appConfig, authConfig) => {
63
+ const products = _getProducts(appConfig);
64
+ const packages = defaults.packages(appConfig, products);
65
+ const packageIds = packages?.map((pkg) => pkg.ordId) || [];
66
+ const entityTypes = _getEntityTypes(appConfig, packageIds);
67
+ const integrationDependencies = getIntegrationDependencies(linkedCsn, appConfig, packageIds);
68
+ const apiResources = _getAPIResources(linkedCsn, appConfig, packageIds, authConfig.accessStrategies);
69
+ const eventResources = _getEventResources(linkedCsn, appConfig, packageIds, authConfig.accessStrategies);
70
+
71
+ return {
72
+ // Unconditionally added top-level properties
283
73
  $schema: "https://open-resource-discovery.github.io/specification/spec-v1/interfaces/Document.schema.json",
284
- openResourceDiscovery: _getOpenResourceDiscovery(appConfig),
285
74
  policyLevels: appConfig.policyLevels,
286
- description: _getDescription(appConfig),
287
- consumptionBundles: _getConsumptionBundles(appConfig),
75
+ packages: packages,
76
+ description: appConfig.env?.description || defaults.description,
77
+ openResourceDiscovery: appConfig.env?.openResourceDiscovery || defaults.openResourceDiscovery,
78
+ consumptionBundles: appConfig.env?.consumptionBundles || defaults.consumptionBundles(appConfig),
79
+
80
+ // Conditionally added top-level properties
81
+ ...(!entityTypes.length ? {} : { entityTypes: entityTypes }),
82
+ ...(!apiResources.length ? {} : { apiResources: apiResources }),
83
+ ...(!eventResources.length ? {} : { eventResources: eventResources }),
84
+ ...(appConfig.existingProductORDId ? {} : { products: [products[0]] }),
85
+ ...(!appConfig.serviceNames.length ? {} : { groups: _getGroups(linkedCsn, appConfig) }),
86
+ ...(!integrationDependencies.length ? {} : { integrationDependencies: integrationDependencies }),
288
87
  };
88
+ };
289
89
 
290
- if (appConfig.serviceNames.length) {
291
- ordDocument.groups = _getGroups(linkedCsn, appConfig);
292
- }
293
-
294
- if (appConfig.env?.existingProductORDId) {
295
- appConfig.existingProductORDId = appConfig.env.existingProductORDId;
296
- } else {
297
- ordDocument.products = [_getProducts(appConfig)[0]];
298
- }
299
-
300
- ordDocument.packages = _getPackages(appConfig);
301
-
302
- return ordDocument;
303
- }
304
-
305
- function extractPackageIds(ordDocument) {
306
- return ordDocument.packages?.map((pkg) => pkg.ordId) || [];
307
- }
308
-
309
- function _filterUnusedPackages(ordDocument) {
90
+ const _filterUnusedPackages = (ordDocument) => {
310
91
  if (!ordDocument.packages?.length) return [];
311
92
 
312
93
  const usedPackageIds = new Set();
@@ -322,52 +103,33 @@ function _filterUnusedPackages(ordDocument) {
322
103
  });
323
104
 
324
105
  return ordDocument.packages.filter((pkg) => usedPackageIds.has(pkg.ordId));
325
- }
106
+ };
326
107
 
327
- module.exports = (csn, extensions = []) => {
328
- const linkedCsn = _propagateORDVisibility(cds.linked(csn));
329
- const appConfig = initializeAppConfig(linkedCsn);
108
+ const _getEntityTypes = (appConfig, packageIds) => {
109
+ return appConfig.entityTypeTargets.flatMap((entity) => createEntityTypeTemplate(appConfig, packageIds, entity));
110
+ };
330
111
 
331
- // Create auth config and fail-closed on configuration errors
112
+ const _createAuthConfig = () => {
332
113
  const authConfig = createAuthConfig();
114
+
115
+ // Create auth config and fail-closed on configuration errors
333
116
  if (authConfig.error) {
334
117
  throw new Error(`Authentication configuration error: ${authConfig.error}`);
335
118
  }
336
- const accessStrategies = authConfig.accessStrategies;
337
-
338
- let ordDocument = createDefaultORDDocument(linkedCsn, appConfig);
339
- const packageIds = extractPackageIds(ordDocument);
340
- const entityTypes = _getEntityTypes(appConfig, packageIds);
341
119
 
342
- if (entityTypes.length) {
343
- ordDocument.entityTypes = entityTypes;
344
- }
345
-
346
- if (appConfig.apiResourceNames.length) {
347
- const apiResources = _getAPIResources(linkedCsn, appConfig, packageIds, accessStrategies);
348
- if (apiResources.length) {
349
- ordDocument.apiResources = apiResources;
350
- }
351
- }
352
- if (appConfig.eventServiceNames.length) {
353
- const eventResources = _getEventResources(linkedCsn, appConfig, packageIds, accessStrategies);
354
- if (eventResources.length) {
355
- ordDocument.eventResources = eventResources;
356
- }
357
- }
358
-
359
- const integrationDependencies = getIntegrationDependencies(linkedCsn, appConfig, packageIds);
360
- if (integrationDependencies.length) {
361
- ordDocument.integrationDependencies = integrationDependencies;
362
- }
120
+ return authConfig;
121
+ };
363
122
 
364
- [...(extensions || []), getCustomORDContent(appConfig)]
123
+ module.exports = (csn, extensions = []) => {
124
+ const authConfig = _createAuthConfig();
125
+ const linkedCsn = _propagateORDVisibility(cds.linked(csn));
126
+ const appConfig = new Configuration(linkedCsn);
127
+ const ordDocument = [...(extensions || []), getCustomORDContent(appConfig)]
365
128
  .filter((extension) => !!extension)
366
- .forEach((extension) => {
367
- ordDocument = compareAndHandleCustomORDContentWithExistingContent(ordDocument, extension);
368
- });
369
-
370
- ordDocument.packages = _filterUnusedPackages(ordDocument);
129
+ .reduce(
130
+ (document, extension) => compareAndHandleCustomORDContentWithExistingContent(document, extension),
131
+ _createDefaultORDDocument(linkedCsn, appConfig, authConfig),
132
+ );
371
133
 
372
- return ordDocument;
134
+ return Object.assign(ordDocument, { packages: _filterUnusedPackages(ordDocument) });
373
135
  };
@@ -1,19 +1,24 @@
1
1
  const cds = require("@sap/cds/lib");
2
2
  const { ord, getMetadata } = require("@cap-js/ord/lib");
3
3
 
4
+ const defaults = require("../defaults");
5
+ const { DOCUMENT_PERSPECTIVES } = require("../constants");
6
+
4
7
  module.exports = class MtxOrdProviderService extends cds.ApplicationService {
5
8
  init() {
6
9
  this.on("getOrdDocument", async (req) => {
7
10
  req._?.res?.set("Content-Type", "application/json");
8
11
 
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
- }),
12
+ return defaults.adjustForPerspective(
13
+ ord(
14
+ await (
15
+ await cds.connect.to("cds.xt.ModelProviderService")
16
+ ).getCsn({
17
+ tenant: req.data.tenant,
18
+ toggles: req.data.toggles,
19
+ }),
20
+ ),
21
+ DOCUMENT_PERSPECTIVES.SystemInstance,
17
22
  );
18
23
  });
19
24
 
@@ -23,7 +28,6 @@ module.exports = class MtxOrdProviderService extends cds.ApplicationService {
23
28
  await (
24
29
  await cds.connect.to("cds.xt.ModelProviderService")
25
30
  ).getCsn({
26
- for: req.data.for,
27
31
  tenant: req.data.tenant,
28
32
  toggles: req.data.toggles,
29
33
  }),
@@ -3,9 +3,61 @@ const cds = require("@sap/cds");
3
3
  const ord = require("../ord.js");
4
4
  const Logger = require("../logger.js");
5
5
  const defaults = require("../defaults.js");
6
+ const { LOCAL_TENANT_ID_HEADER_KEY, DOCUMENT_PERSPECTIVES } = require("../constants");
6
7
  const compileMetadata = require("../meta-data.js");
7
8
  const { createAuthConfig, createAuthMiddleware } = require("../auth/authentication.js");
8
9
 
10
+ const validationMiddleware = (req, res, next) => {
11
+ const toggles = cds.env.requires.toggles;
12
+ const perspective = req.query.perspective;
13
+ const extensibility = cds.env.requires.extensibility;
14
+ const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
15
+
16
+ if (perspective && ![DOCUMENT_PERSPECTIVES.SystemVersion, DOCUMENT_PERSPECTIVES.SystemInstance].includes(perspective)) {
17
+ return res.status(400).send(`Required query parameter 'perspective' is invalid`);
18
+ }
19
+
20
+ if (perspective === DOCUMENT_PERSPECTIVES.SystemInstance && !(toggles || extensibility)) {
21
+ return res.status(400).send(`Unsupported query parameter 'perspective=${perspective}'`);
22
+ }
23
+
24
+ if (!tenant && perspective === DOCUMENT_PERSPECTIVES.SystemInstance) {
25
+ return res.status(400).send(`Missing required tenant context`);
26
+ }
27
+
28
+ return next();
29
+ };
30
+
31
+ const metadataResponseHandler = async (req, res) => {
32
+ try {
33
+ const perspective = req.query.perspective;
34
+ const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
35
+ const model = await resolveCdsModel(perspective, tenant);
36
+ const { contentType, response } = await compileMetadata(req.path, model);
37
+
38
+ return res.status(200).contentType(contentType).send(response);
39
+ } catch (error) {
40
+ Logger.error(error, "Error while processing the resource definition document");
41
+ return res.status(500).send(error.message);
42
+ }
43
+ };
44
+
45
+ const resolveCdsModel = async (perspective, tenant) => {
46
+ if (!tenant || perspective !== DOCUMENT_PERSPECTIVES.SystemInstance) {
47
+ Logger.info("Retrieving static CDS model...");
48
+ return cds.model;
49
+ }
50
+
51
+ Logger.info(`Retrieving dynamic CDS model for tenant ${tenant}...`);
52
+
53
+ return await (
54
+ await cds.connect.to("cds.xt.ModelProviderService")
55
+ ).getCsn({
56
+ tenant: tenant,
57
+ toggles: OpenResourceDiscoveryService.resolveFeatureToggles(tenant),
58
+ });
59
+ };
60
+
9
61
  class OpenResourceDiscoveryService extends cds.ApplicationService {
10
62
  async init() {
11
63
  this.extensions = {};
@@ -26,38 +78,38 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
26
78
  // Create authentication middleware
27
79
  const authMiddleware = createAuthMiddleware(authConfig);
28
80
 
29
- cds.app.get(`${this.path}`, (_, res) => {
30
- return res.status(200).send(defaults.baseTemplate(authConfig));
81
+ // Default: /.well-known/open-resource-discovery
82
+ cds.app.get(`${this.path}`, (req, res) => {
83
+ const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
84
+
85
+ return res.status(200).send(defaults.baseTemplate(authConfig, tenant));
31
86
  });
32
87
 
33
- cds.app.get(`/ord/v1/documents/ord-document`, authMiddleware, async (_, res) => {
34
- const csn = cds.context?.model || cds.model;
35
- const data = ord(csn, Array.from(Object.values(this.extensions)));
36
- return res.status(200).send(data);
88
+ cds.app.get(`/ord/v1/documents/ord-document`, [authMiddleware, validationMiddleware], async (req, res) => {
89
+ const perspective = req.query.perspective || DOCUMENT_PERSPECTIVES.SystemVersion;
90
+ const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
91
+ const model = await resolveCdsModel(perspective, tenant);
92
+ const extensions = Array.from(Object.values(this.extensions));
93
+
94
+ return res.status(200).send(defaults.adjustForPerspective(ord(model, extensions), perspective));
37
95
  });
38
96
 
39
- cds.app.get(`/ord/v1/documents/:id`, authMiddleware, async (_, res) => {
97
+ cds.app.get(`/ord/v1/documents/:id`, [authMiddleware, validationMiddleware], async (_, res) => {
40
98
  return res.status(404).send("404 Not Found");
41
99
  });
42
100
 
43
- // Handler for metadata requests (oas3, edmx, csn, etc.)
44
- const metadataHandler = async (req, res) => {
45
- try {
46
- const { contentType, response } = await compileMetadata(req.url);
47
- return res.status(200).contentType(contentType).send(response);
48
- } catch (error) {
49
- Logger.error(error, "Error while processing the resource definition document");
50
- return res.status(500).send(error.message);
51
- }
52
- };
53
-
54
101
  // Use separate routes instead of optional parameters for path-to-regexp v8 compatibility
55
- cds.app.get(`/ord/v1/:ordId/:service`, authMiddleware, metadataHandler);
56
- cds.app.get(`/ord/v1/:ordId`, authMiddleware, metadataHandler);
57
- cds.app.get(`/ord/v1`, authMiddleware, metadataHandler);
102
+ cds.app.get(`/ord/v1/:ordId/:service`, [authMiddleware, validationMiddleware], metadataResponseHandler);
103
+ cds.app.get(`/ord/v1/:ordId`, [authMiddleware, validationMiddleware], metadataResponseHandler);
104
+ cds.app.get(`/ord/v1`, [authMiddleware, validationMiddleware], metadataResponseHandler);
58
105
 
59
106
  return super.init();
60
107
  }
108
+
109
+ // eslint-disable-next-line no-unused-vars
110
+ static resolveFeatureToggles(tenant) {
111
+ return ["*"];
112
+ }
61
113
  }
62
114
 
63
115
  module.exports = { OpenResourceDiscoveryService };
package/lib/templates.js CHANGED
@@ -1,4 +1,3 @@
1
- const { hasSAPPolicyLevel } = require("./utils");
2
1
  const defaults = require("./defaults");
3
2
  const _ = require("lodash");
4
3
  const {
@@ -76,14 +75,22 @@ const createEntityTypeMappingsItemTemplate = (entity) => {
76
75
  };
77
76
 
78
77
  function _getGroupID(serviceDefinition, groupTypeId = defaults.groupTypeId, appConfig) {
79
- return `${groupTypeId}:${appConfig.ordNamespace}:${_getGroupNameWithNestedNamespace(serviceDefinition, appConfig)}`;
78
+ return `${groupTypeId}:${appConfig.ordNamespace}:${_getCleanServiceName(serviceDefinition, appConfig)}`;
80
79
  }
81
80
 
82
- function _getGroupNameWithNestedNamespace({ name }, appConfig) {
83
- if (!name.startsWith(appConfig.ordNamespace)) {
84
- return name;
81
+ function _startsWithNamespace(name, namespace) {
82
+ if (!name.startsWith(namespace)) return false;
83
+ const rest = name.substring(namespace.length);
84
+ return rest === "" || rest.startsWith(".");
85
+ }
86
+
87
+ function _getCleanServiceName({ name }, appConfig) {
88
+ let sortedName = name;
89
+ if (appConfig.internalNamespace && _startsWithNamespace(name, appConfig.internalNamespace)) {
90
+ sortedName = name.substring(appConfig.internalNamespace.length);
91
+ } else if (_startsWithNamespace(name, appConfig.ordNamespace)) {
92
+ sortedName = name.substring(appConfig.ordNamespace.length);
85
93
  }
86
- let sortedName = name.substring(appConfig.ordNamespace.length);
87
94
  if (sortedName.startsWith(".")) {
88
95
  sortedName = sortedName.substring(1);
89
96
  }
@@ -157,7 +164,7 @@ function _getResourceDefinition(resourceType, mediaType, ordId, serviceName, fil
157
164
  *
158
165
  * @param {string} serviceName The name of the service.
159
166
  * @param {object} serviceDefinition The definition of the service
160
- * @param {Set} groupIds A set of group ids.
167
+ * @param {object} appConfig - The application configuration.
161
168
  * @returns {Object} A group object.
162
169
  */
163
170
  const createGroupsTemplateForService = (serviceName, serviceDefinition, appConfig) => {
@@ -197,7 +204,7 @@ const createEntityTypeTemplate = (appConfig, packageIds, entity) => {
197
204
  // ODM mappings are not created as entity types, they are only used in entityTypeMappings
198
205
  return [];
199
206
  }
200
- if (hasSAPPolicyLevel(appConfig.policyLevels)) {
207
+ if (appConfig.hasSAPPolicyLevel) {
201
208
  // If SAP policy level is present, don't create entity type, they must be in the central repository
202
209
  return [];
203
210
  }
@@ -305,19 +312,19 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
305
312
  extracted = _extractVersionFromServiceName(serviceDefinition.name);
306
313
  if (extracted) {
307
314
  // Create a temporary service definition with the clean name for namespace processing
308
- const cleanServiceDefinition = { ...serviceDefinition, name: extracted.cleanName };
309
- cleanServiceName = _getGroupNameWithNestedNamespace(cleanServiceDefinition, appConfig);
315
+ const versionExtractedServiceDefinition = { ...serviceDefinition, name: extracted.cleanName };
316
+ cleanServiceName = _getCleanServiceName(versionExtractedServiceDefinition, appConfig);
310
317
  version = extracted.version;
311
318
  semanticVersion = extracted.semanticVersion;
312
319
  } else {
313
320
  // Invalid pattern - use current behavior
314
- cleanServiceName = _getGroupNameWithNestedNamespace(serviceDefinition, appConfig);
321
+ cleanServiceName = _getCleanServiceName(serviceDefinition, appConfig);
315
322
  version = "v1";
316
323
  semanticVersion = "1.0.0";
317
324
  }
318
325
  } else {
319
326
  // Non-data product - use current behavior
320
- cleanServiceName = _getGroupNameWithNestedNamespace(serviceDefinition, appConfig);
327
+ cleanServiceName = _getCleanServiceName(serviceDefinition, appConfig);
321
328
  version = "v1";
322
329
  semanticVersion = "1.0.0";
323
330
  }
@@ -349,7 +356,14 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
349
356
  ];
350
357
  } else if (apiProtocol === ORD_API_PROTOCOL.MCP) {
351
358
  resourceDefinitions = [
352
- _getResourceDefinition(MCP_RESOURCE_DEFINITION_TYPE, "json", ordId, serviceName, "mcp.json", accessStrategies),
359
+ _getResourceDefinition(
360
+ MCP_RESOURCE_DEFINITION_TYPE,
361
+ "json",
362
+ ordId,
363
+ serviceName,
364
+ "mcp.json",
365
+ accessStrategies,
366
+ ),
353
367
  ];
354
368
  } else if (apiProtocol === ORD_API_PROTOCOL.GRAPHQL) {
355
369
  // GraphQL only has GraphQL SDL
@@ -363,8 +377,13 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
363
377
  }),
364
378
  },
365
379
  ];
380
+ } else if (apiProtocol === ORD_API_PROTOCOL.ODATA_V2) {
381
+ // openapi-v3 is not supported for OData V2, only EDMX
382
+ resourceDefinitions = [
383
+ _getResourceDefinition("edmx", "xml", ordId, serviceName, "edmx", accessStrategies),
384
+ ];
366
385
  } else {
367
- // OData and others have both OpenAPI and EDMX
386
+ // odata-v4 and others have both OpenAPI and EDMX
368
387
  resourceDefinitions = [
369
388
  _getResourceDefinition("openapi-v3", "json", ordId, serviceName, "oas3.json", accessStrategies),
370
389
  _getResourceDefinition("edmx", "xml", ordId, serviceName, "edmx", accessStrategies),
@@ -372,7 +391,6 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
372
391
  }
373
392
  }
374
393
 
375
- const entityTypeMappings = _getEntityTypeMappings(serviceDefinition);
376
394
  const exposedEntityTypes = _getExposedEntityTypes(serviceDefinition);
377
395
 
378
396
  let obj = {
@@ -392,7 +410,6 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
392
410
  extensible: {
393
411
  supported: "no",
394
412
  },
395
- ...(entityTypeMappings ? { entityTypeMappings } : {}),
396
413
  ...(exposedEntityTypes ? { exposedEntityTypes } : []),
397
414
  ...ordExtensions,
398
415
  };
@@ -431,8 +448,7 @@ const createEventResourceTemplate = (serviceName, serviceDefinition, appConfig,
431
448
  const ordExtensions = readORDExtensions(serviceDefinition);
432
449
  const visibility = _handleVisibility(ordExtensions, serviceDefinition, appConfig.env?.defaultVisibility);
433
450
  const packageId = _getPackageID(appConfig.ordNamespace, packageIds, ORD_RESOURCE_TYPE.event, visibility);
434
- const ordId = `${appConfig.ordNamespace}:eventResource:${_getGroupNameWithNestedNamespace(serviceDefinition, appConfig)}:v1`;
435
- const entityTypeMappings = _getEntityTypeMappings(serviceDefinition);
451
+ const ordId = `${appConfig.ordNamespace}:eventResource:${_getCleanServiceName(serviceDefinition, appConfig)}:v1`;
436
452
  const exposedEntityTypes = _getExposedEntityTypes(serviceDefinition);
437
453
 
438
454
  let obj = {
@@ -456,7 +472,6 @@ const createEventResourceTemplate = (serviceName, serviceDefinition, appConfig,
456
472
  _getResourceDefinition("asyncapi-v2", "json", ordId, serviceName, "asyncapi2.json", accessStrategies),
457
473
  ],
458
474
  extensible: { supported: "no" },
459
- ...(entityTypeMappings ? { entityTypeMappings } : {}),
460
475
  ...(exposedEntityTypes ? { exposedEntityTypes } : []),
461
476
  ...ordExtensions,
462
477
  };
@@ -473,28 +488,6 @@ function isPrimaryDataProductService(serviceDefinition) {
473
488
  );
474
489
  }
475
490
 
476
- function _getEntityTypeMappings(definitionObj) {
477
- if (!definitionObj.entities) {
478
- return;
479
- }
480
- const entities = Object.values(definitionObj.entities).flatMap((entity) => {
481
- const entityData = _flattenEntityGraph(entity)
482
- .flatMap(createEntityTypeMappingsItemTemplate) // now returns arrays
483
- .filter(Boolean);
484
- return _.uniqBy(entityData, CONTENT_MERGE_KEY);
485
- });
486
- const entityTypeTargets = _.uniqBy(entities, CONTENT_MERGE_KEY)
487
- .filter((entity) => entity !== undefined)
488
- .map(({ ordId }) => ({
489
- ordId,
490
- }));
491
- if (entityTypeTargets.length > 0) {
492
- return [{ entityTypeTargets }];
493
- } else {
494
- return;
495
- }
496
- }
497
-
498
491
  function _getExposedEntityTypes(definitionObj) {
499
492
  if (!definitionObj.entities) {
500
493
  return;
@@ -508,10 +501,9 @@ function _getExposedEntityTypes(definitionObj) {
508
501
  .map(({ ordId }) => ({
509
502
  ordId,
510
503
  }));
504
+
511
505
  if (exposedEntityTypes.length > 0) {
512
506
  return exposedEntityTypes;
513
- } else {
514
- return;
515
507
  }
516
508
  }
517
509
 
@@ -614,7 +606,6 @@ module.exports = {
614
606
  createEventResourceTemplate,
615
607
  readORDExtensions,
616
608
  _getPackageID,
617
- _getEntityTypeMappings,
618
609
  _getExposedEntityTypes,
619
610
  _propagateORDVisibility,
620
611
  _handleVisibility,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/ord",
3
- "version": "1.6.0",
3
+ "version": "1.7.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)",
@@ -25,7 +25,8 @@
25
25
  "test:integration:mtls": "jest __tests__/integration/mtls-auth.test.js --testPathIgnorePatterns=/node_modules/ --forceExit",
26
26
  "test:integration:build": "jest __tests__/integration/cds-build.test.js --testPathIgnorePatterns=/node_modules/ --forceExit",
27
27
  "update-snapshot": "jest --ci --updateSnapshot",
28
- "cds:version": "cds v -i"
28
+ "cds:version": "cds v -i",
29
+ "test:all": "npm run test && npm run test:integration:basic && npm run test:integration:mtls && npm run test:integration:build"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@cap-js/graphql": "^0.14.0",
@@ -52,7 +53,7 @@
52
53
  "node": ">=20 <25"
53
54
  },
54
55
  "overrides": {
55
- "@sap/cds-compiler": "6.9.0"
56
+ "@sap/cds-compiler": "6.9.2"
56
57
  },
57
58
  "cds": {
58
59
  "requires": {
package/lib/utils.js DELETED
@@ -1,12 +0,0 @@
1
- /**
2
- * Checks if at least one policy level in the array is SAP.
3
- *
4
- * @param {string[]} policyLevels - Array of policy levels.
5
- */
6
- function hasSAPPolicyLevel(policyLevels) {
7
- return policyLevels.some((policyLevel) => policyLevel.split(":")[0].toLowerCase() === "sap");
8
- }
9
-
10
- module.exports = {
11
- hasSAPPolicyLevel,
12
- };