@cap-js/ord 1.2.0 → 1.3.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/ord.js CHANGED
@@ -1,85 +1,175 @@
1
- const { ORD_RESOURCE_TYPE } = require('./constants');
1
+ 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");
2
8
  const {
3
9
  createAPIResourceTemplate,
4
10
  createEntityTypeTemplate,
11
+ createEntityTypeMappingsItemTemplate,
5
12
  createEventResourceTemplate,
6
- createGroupsTemplateForService
7
- } = require('./templates');
8
- const { extendCustomORDContentIfExists } = require('./extendOrdWithCustom');
9
- const { getRFC3339Date } = require('./date');
10
- const { Logger } = require('./logger');
13
+ createGroupsTemplateForService,
14
+ _propagateORDVisibility,
15
+ } = require("./templates");
16
+ const { extendCustomORDContentIfExists } = require("./extendOrdWithCustom");
17
+ const { getRFC3339Date } = require("./date");
18
+ const { getAuthConfig } = require("./authentication");
19
+
20
+ const { Logger } = require("./logger");
11
21
  const _ = require("lodash");
12
22
  const cds = require("@sap/cds");
13
23
  const defaults = require("./defaults");
14
24
  const path = require("path");
15
25
 
16
26
  const initializeAppConfig = (csn) => {
17
- let packageJsonPath = path.join(cds.root, 'package.json')
18
- let packageJson;
19
- if (cds.utils.exists(packageJsonPath)) {
20
- packageJson = require(packageJsonPath);
21
- } else {
22
- throw new Error(`package.json not found in the project root directory`);
23
- }
27
+ const packageJson = _loadPackageJson();
24
28
  const packageName = packageJson.name;
25
- const appName = packageJson.name.replace(/^[@]/, "").replace(/[@/]/g, "-");
26
- const modelKeys = Object.keys(csn.definitions);
27
- const events = [];
28
- const serviceNames = [];
29
+ const appName = _formatAppName(packageName);
29
30
  const lastUpdate = getRFC3339Date();
30
- let odmEntity = [];
31
31
 
32
- const vendorNamespace = "customer";
33
- const ordNamespace = cds.env["ord"]?.namespace || `${vendorNamespace}.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
32
+ const ordNamespace = _getORDNamespace(packageName);
34
33
  const eventApplicationNamespace = cds.env?.export?.asyncapi?.applicationNamespace;
35
34
 
36
- if (eventApplicationNamespace && ordNamespace !== eventApplicationNamespace) {
37
- Logger.warn('ORD and AsyncAPI namespaces should be the same.');
38
- }
35
+ _validateNamespaces(ordNamespace, eventApplicationNamespace);
39
36
 
40
- for (const key of modelKeys) {
41
- const keyDefinition = csn.definitions[key];
42
- switch (keyDefinition.kind) {
43
- case ORD_RESOURCE_TYPE.service:
44
- if (!keyDefinition["@cds.external"]) {
45
- serviceNames.push(key);
46
- }
47
- break;
48
- // TODO: should be rewritten
49
- case ORD_RESOURCE_TYPE.entity:
50
- if (!key.includes(".texts") && keyDefinition["@ODM.entityName"]) {
51
- odmEntity.push(createEntityTypeTemplate(keyDefinition));
52
- }
53
- break;
54
- case ORD_RESOURCE_TYPE.event:
55
- events.push(key);
56
- break;
57
- }
58
- }
59
-
60
- odmEntity = _.uniqBy(odmEntity, "ordId");
37
+ const { serviceNames, apiResourceNames, apiEndpoints, eventNames, entityTypeTargets } = _triageCsnDefinitions(csn);
61
38
 
62
39
  return {
63
40
  env: cds.env["ord"],
64
41
  lastUpdate,
65
42
  appName,
66
- events,
43
+ apiEndpoints: Array.from(apiEndpoints),
44
+ eventNames,
67
45
  serviceNames,
68
- odmEntity,
46
+ apiResourceNames,
47
+ entityTypeTargets: _.uniqBy(entityTypeTargets, CONTENT_MERGE_KEY),
69
48
  ordNamespace,
70
49
  eventApplicationNamespace,
71
50
  packageName,
72
51
  };
73
52
  };
74
53
 
75
- const _getPolicyLevel = (appConfig) =>
76
- appConfig.env?.policyLevel || defaults.policyLevel;
54
+ function _loadPackageJson() {
55
+ const packageJsonPath = path.join(cds.root, "package.json");
56
+ if (!cds.utils.exists(packageJsonPath)) {
57
+ throw new Error(`package.json not found in the project root directory`);
58
+ }
59
+ return require(packageJsonPath);
60
+ }
61
+
62
+ function _formatAppName(packageName) {
63
+ return packageName.replace(/^[@]/, "").replace(/[@/]/g, "-");
64
+ }
65
+
66
+ function _getORDNamespace(packageName) {
67
+ const vendorNamespace = "customer";
68
+ return cds.env["ord"]?.namespace || `${vendorNamespace}.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
69
+ }
70
+
71
+ function _validateNamespaces(ordNamespace, eventApplicationNamespace) {
72
+ if (eventApplicationNamespace && ordNamespace !== eventApplicationNamespace) {
73
+ Logger.warn("ORD and AsyncAPI namespaces should be the same.");
74
+ }
75
+ }
76
+
77
+ function _triageCsnDefinitions(csn) {
78
+ const pendingApiResourceNames = [];
79
+ const apiEndpoints = new Set();
80
+ const pendingEventNames = [];
81
+ const entityTypeTargets = [];
82
+ const pendingServiceNames = [];
83
+
84
+ for (const definitionKey of Object.keys(csn.definitions)) {
85
+ const definitionObj = csn.definitions[definitionKey];
86
+ if (definitionKey.includes(BLOCKED_SERVICE_NAME.MTXServices)) {
87
+ Logger.warn(`ORD service name "${definitionKey}" is blocked.`);
88
+ continue;
89
+ }
90
+ switch (definitionObj.kind) {
91
+ case CDS_ELEMENT_KIND.service: {
92
+ pendingServiceNames.push(definitionKey);
93
+ const apiResourceName = _handleApiResource(definitionKey, definitionObj);
94
+ if (apiResourceName) pendingApiResourceNames.push(apiResourceName);
95
+ break;
96
+ }
97
+ case CDS_ELEMENT_KIND.entity: {
98
+ const result = _handleEntity(definitionKey, definitionObj);
99
+ if (result) {
100
+ if (result.apiEndpoint) apiEndpoints.add(result.apiEndpoint);
101
+ if (result.entityTypeTarget) entityTypeTargets.push(result.entityTypeTarget);
102
+ }
103
+ break;
104
+ }
105
+ case CDS_ELEMENT_KIND.event: {
106
+ const event = _handleEvent(definitionKey);
107
+ if (event) pendingEventNames.push(event);
108
+ break;
109
+ }
110
+ case CDS_ELEMENT_KIND.action:
111
+ case CDS_ELEMENT_KIND.function: {
112
+ const apiEndpoint = _handleActionOrFunction(definitionKey);
113
+ if (apiEndpoint) apiEndpoints.add(apiEndpoint);
114
+ break;
115
+ }
116
+ }
117
+ }
118
+
119
+ return {
120
+ serviceNames: pendingServiceNames,
121
+ apiResourceNames: pendingApiResourceNames,
122
+ apiEndpoints: Array.from(apiEndpoints),
123
+ eventNames: pendingEventNames,
124
+ entityTypeTargets,
125
+ };
126
+ }
127
+
128
+ function _handleApiResource(apiResourceName, serviceDefinition) {
129
+ if (_shouldSkipIfServiceOnlyContainsEvents(serviceDefinition) || serviceDefinition["@cds.external"]) {
130
+ return null;
131
+ }
132
+ return apiResourceName;
133
+ }
77
134
 
78
- const _getDescription = (appConfig) =>
79
- appConfig.env?.description || defaults.description;
135
+ function _shouldSkipIfServiceOnlyContainsEvents(serviceDefinition) {
136
+ const isActionsNotContained = !serviceDefinition.actions || Object.keys(serviceDefinition.actions).length === 0;
137
+ const isFunctionsNotContained =
138
+ !serviceDefinition.functions || Object.keys(serviceDefinition.functions).length === 0;
139
+ const isEntitiesNotContained = !serviceDefinition.entities || Object.keys(serviceDefinition.entities).length === 0;
140
+ const isEventsContained = serviceDefinition.events && Object.keys(serviceDefinition.events).length > 0;
141
+ if (isActionsNotContained && isFunctionsNotContained && isEntitiesNotContained && isEventsContained) {
142
+ return true;
143
+ }
144
+ return false;
145
+ }
80
146
 
81
- const _getProducts = (appConfig) =>
82
- appConfig.env?.products || defaults.products(appConfig.packageName);
147
+ function _handleEntity(key, keyDefinition) {
148
+ if (!key.includes(".texts")) {
149
+ const apiEndpoint = key;
150
+ const entityTypeTarget =
151
+ keyDefinition[ORD_ODM_ENTITY_NAME_ANNOTATION] || keyDefinition[ENTITY_RELATIONSHIP_ANNOTATION]
152
+ ? createEntityTypeMappingsItemTemplate(keyDefinition)
153
+ : null;
154
+ return { apiEndpoint, entityTypeTarget };
155
+ }
156
+ return null;
157
+ }
158
+
159
+ function _handleEvent(key) {
160
+ return key;
161
+ }
162
+
163
+ function _handleActionOrFunction(key) {
164
+ return key;
165
+ }
166
+
167
+ const _getPolicyLevels = (appConfig) =>
168
+ appConfig.env?.policyLevels ||
169
+ (appConfig.env?.policyLevel && [appConfig.env?.policyLevel]) ||
170
+ defaults.policyLevels;
171
+
172
+ const _getDescription = (appConfig) => appConfig.env?.description || defaults.description;
83
173
 
84
174
  const _getGroups = (csn, appConfig) => {
85
175
  return appConfig.serviceNames
@@ -87,23 +177,32 @@ const _getGroups = (csn, appConfig) => {
87
177
  .filter((resource) => !!resource);
88
178
  };
89
179
 
90
- const _getPackages = (policyLevel, appConfig) =>
91
- appConfig.env?.packages || appConfig.events.length
92
- ? defaults.packages(appConfig.appName, policyLevel, appConfig.ordNamespace)
93
- : defaults
94
- .packages(appConfig.appName, policyLevel, appConfig.ordNamespace)
95
- .slice(0, 1);
180
+ const _getPackages = (policyLevels, appConfig) => {
181
+ return defaults.packages(appConfig, policyLevels);
182
+ };
96
183
 
97
- const _getAPIResources = (csn, appConfig, packageIds) => {
98
- return appConfig.serviceNames
99
- .flatMap((serviceName) => createAPIResourceTemplate(serviceName, csn.definitions[serviceName], appConfig, packageIds))
184
+ const _getEntityTypes = (appConfig, packageIds) => {
185
+ if (!appConfig.entityTypeTargets?.length) return [];
186
+
187
+ return appConfig.entityTypeTargets.map((entity) => createEntityTypeTemplate(appConfig, packageIds, entity));
100
188
  };
101
189
 
102
- const _getEventResources = (csn, appConfig, packageIds) => {
103
- if (appConfig.events.length === 0) return [];
104
- return appConfig.serviceNames
105
- .filter((serviceName) => appConfig.events.some((eventName) => eventName.startsWith(serviceName)))
106
- .flatMap((serviceName) => createEventResourceTemplate(serviceName, csn.definitions[serviceName], appConfig, packageIds));
190
+ const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
191
+ return appConfig.apiResourceNames.flatMap((apiResourceName) =>
192
+ createAPIResourceTemplate(
193
+ apiResourceName,
194
+ csn.definitions[apiResourceName],
195
+ appConfig,
196
+ packageIds,
197
+ accessStrategies,
198
+ ),
199
+ );
200
+ };
201
+
202
+ const _getEventResources = (csn, appConfig, packageIds, accessStrategies) => {
203
+ return appConfig.serviceNames.flatMap((serviceName) =>
204
+ createEventResourceTemplate(serviceName, csn.definitions[serviceName], appConfig, packageIds, accessStrategies),
205
+ );
107
206
  };
108
207
 
109
208
  function _getOpenResourceDiscovery(appConfig) {
@@ -114,55 +213,96 @@ function _getConsumptionBundles(appConfig) {
114
213
  return appConfig.env?.consumptionBundles || defaults.consumptionBundles(appConfig);
115
214
  }
116
215
 
117
- function validateNamespace(appConfig) {
118
- const validateSystemNamespace = new RegExp(`^${appConfig.eventApplicationNamespace}\\.[^.]+\\..+$`);
119
- if (
120
- appConfig.ordNamespace === undefined &&
121
- !validateSystemNamespace.test(appConfig.ordNamespace)
122
- ) {
123
- let error = new Error(
124
- `Namespace is not defined in cdsrc.json or it is not in the format of ${appConfig.eventApplicationNamespace}.<appName>.<service>`
125
- );
126
- Logger.error('Namespace error:', error.message);
127
- throw error;
216
+ const _getProducts = (appConfig) => {
217
+ const productsObj = defaults.products(appConfig.packageName);
218
+ if (appConfig.env?.products) {
219
+ const customProducts = appConfig.env.products[0];
220
+ if (customProducts?.ordId?.toLowerCase().startsWith("sap")) {
221
+ Logger.error(
222
+ "Detected sap product ordId, which should not be defined for custom products, use default value instead. Please check ord global registry.",
223
+ );
224
+ } else {
225
+ _.assign(productsObj[0], customProducts);
226
+ }
128
227
  }
129
- }
228
+ appConfig.products = productsObj;
229
+ return productsObj;
230
+ };
130
231
 
131
232
  function createDefaultORDDocument(linkedCsn, appConfig) {
132
233
  let ordDocument = {
133
- $schema: "https://sap.github.io/open-resource-discovery/spec-v1/interfaces/Document.schema.json",
234
+ $schema: "https://open-resource-discovery.github.io/specification/spec-v1/interfaces/Document.schema.json",
134
235
  openResourceDiscovery: _getOpenResourceDiscovery(appConfig),
135
- policyLevel: _getPolicyLevel(appConfig),
236
+ policyLevels: _getPolicyLevels(appConfig),
136
237
  description: _getDescription(appConfig),
137
- products: _getProducts(appConfig),
138
- groups: _getGroups(linkedCsn, appConfig),
139
238
  consumptionBundles: _getConsumptionBundles(appConfig),
140
239
  };
141
240
 
142
- if (_getAPIResources(linkedCsn, appConfig).length && _getEventResources(linkedCsn, appConfig).length) {
143
- ordDocument.packages = _getPackages(ordDocument.policyLevel, appConfig);
241
+ if (appConfig.serviceNames.length) {
242
+ ordDocument.groups = _getGroups(linkedCsn, appConfig);
243
+ }
244
+
245
+ if (appConfig.env?.existingProductORDId) {
246
+ appConfig.existingProductORDId = appConfig.env.existingProductORDId;
247
+ } else {
248
+ ordDocument.products = [_getProducts(appConfig)[0]];
144
249
  }
250
+
251
+ ordDocument.packages = _getPackages(ordDocument.policyLevels, appConfig);
252
+
145
253
  return ordDocument;
146
254
  }
147
255
 
148
256
  function extractPackageIds(ordDocument) {
149
- const packageIds = [];
150
- if (ordDocument.packages) {
151
- ordDocument.packages.map((pkg) => packageIds.push(pkg.ordId));
152
- }
153
- return packageIds;
257
+ return ordDocument.packages?.map((pkg) => pkg.ordId) || [];
258
+ }
259
+
260
+ function _filterUnusedPackages(ordDocument) {
261
+ if (!ordDocument.packages?.length) return [];
262
+
263
+ const usedPackageIds = new Set();
264
+
265
+ ordDocument.apiResources?.forEach((api) => usedPackageIds?.add(api.partOfPackage));
266
+ ordDocument.eventResources?.forEach((event) => usedPackageIds?.add(event.partOfPackage));
267
+ ordDocument.dataProducts?.forEach((dp) => usedPackageIds?.add(dp.partOfPackage));
268
+ ordDocument.entityTypes?.forEach((et) => {
269
+ if (et && et.partOfPackage) {
270
+ usedPackageIds.add(et.partOfPackage);
271
+ }
272
+ });
273
+
274
+ const filteredPackages = ordDocument.packages.filter((pkg) => usedPackageIds.has(pkg.ordId));
275
+
276
+ return filteredPackages;
154
277
  }
155
278
 
156
279
  module.exports = (csn) => {
157
- const linkedCsn = cds.linked(csn);
280
+ const linkedCsn = _propagateORDVisibility(cds.linked(csn));
158
281
  const appConfig = initializeAppConfig(linkedCsn);
159
- validateNamespace(appConfig);
282
+ const accessStrategies = getAuthConfig().accessStrategies;
160
283
 
161
284
  let ordDocument = createDefaultORDDocument(linkedCsn, appConfig);
162
285
  const packageIds = extractPackageIds(ordDocument);
163
- ordDocument.apiResources = _getAPIResources(linkedCsn, appConfig, packageIds);
164
- ordDocument.eventResources = _getEventResources(linkedCsn, appConfig, packageIds);
286
+ const entityTypes = _getEntityTypes(appConfig, packageIds);
287
+
288
+ if (entityTypes.length) {
289
+ ordDocument.entityTypes = entityTypes;
290
+ }
291
+
292
+ if (appConfig.apiResourceNames.length) {
293
+ const apiResources = _getAPIResources(linkedCsn, appConfig, packageIds, accessStrategies);
294
+ if (apiResources.length) {
295
+ ordDocument.apiResources = apiResources;
296
+ }
297
+ }
298
+ if (appConfig.eventNames.length) {
299
+ const eventResources = _getEventResources(linkedCsn, appConfig, packageIds, accessStrategies);
300
+ if (eventResources.length) {
301
+ ordDocument.eventResources = eventResources;
302
+ }
303
+ }
165
304
  ordDocument = extendCustomORDContentIfExists(appConfig, ordDocument);
305
+ ordDocument.packages = _filterUnusedPackages(ordDocument);
166
306
 
167
307
  return ordDocument;
168
308
  };