@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/ord.js CHANGED
@@ -1,268 +1,45 @@
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
222
- .flatMap((serviceName) => createGroupsTemplateForService(serviceName, csn.definitions[serviceName], appConfig))
23
+ .map((name) => csn.definitions[name])
24
+ .flatMap((srvDefinition) => createGroupsTemplateForService(srvDefinition, appConfig))
223
25
  .filter((resource) => !!resource);
224
26
  };
225
27
 
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
28
  const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
237
- const apiResources = appConfig.apiResourceNames.flatMap((apiResourceName) =>
238
- createAPIResourceTemplate(
239
- apiResourceName,
240
- csn.definitions[apiResourceName],
241
- appConfig,
242
- packageIds,
243
- accessStrategies,
244
- ),
245
- );
246
-
247
- return apiResources;
29
+ return appConfig.apiResourceNames
30
+ .map((name) => csn.definitions[name])
31
+ .flatMap((srvDefinition) => createAPIResourceTemplate(srvDefinition, appConfig, packageIds, accessStrategies));
248
32
  };
249
33
 
250
34
  const _getEventResources = (csn, appConfig, packageIds, accessStrategies) => {
251
- return appConfig.eventServiceNames.flatMap((serviceName) =>
252
- createEventResourceTemplate(serviceName, csn.definitions[serviceName], appConfig, packageIds, accessStrategies),
253
- );
35
+ return appConfig.eventServiceNames
36
+ .map((name) => csn.definitions[name])
37
+ .flatMap((srvDefinition) => createEventResourceTemplate(srvDefinition, appConfig, packageIds, accessStrategies));
254
38
  };
255
39
 
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
40
  const _getProducts = (appConfig) => {
265
41
  const productsObj = defaults.products(appConfig.packageName);
42
+
266
43
  if (appConfig.env?.products) {
267
44
  const customProducts = appConfig.env.products[0];
268
45
  if (customProducts?.ordId?.toLowerCase().startsWith("sap")) {
@@ -273,40 +50,39 @@ const _getProducts = (appConfig) => {
273
50
  _.assign(productsObj[0], customProducts);
274
51
  }
275
52
  }
276
- appConfig.products = productsObj;
53
+
277
54
  return productsObj;
278
55
  };
279
56
 
280
- function createDefaultORDDocument(linkedCsn, appConfig) {
281
- appConfig.policyLevels = _getPolicyLevels(appConfig);
282
- let ordDocument = {
57
+ const _createDefaultORDDocument = (linkedCsn, appConfig, authConfig) => {
58
+ const products = _getProducts(appConfig);
59
+ const packages = defaults.packages(appConfig, products);
60
+ const packageIds = packages?.map((pkg) => pkg.ordId) || [];
61
+ const entityTypes = _getEntityTypes(appConfig, packageIds);
62
+ const integrationDependencies = getIntegrationDependencies(linkedCsn, appConfig, packageIds);
63
+ const apiResources = _getAPIResources(linkedCsn, appConfig, packageIds, authConfig.accessStrategies);
64
+ const eventResources = _getEventResources(linkedCsn, appConfig, packageIds, authConfig.accessStrategies);
65
+
66
+ return {
67
+ // Unconditionally added top-level properties
283
68
  $schema: "https://open-resource-discovery.github.io/specification/spec-v1/interfaces/Document.schema.json",
284
- openResourceDiscovery: _getOpenResourceDiscovery(appConfig),
285
69
  policyLevels: appConfig.policyLevels,
286
- description: _getDescription(appConfig),
287
- consumptionBundles: _getConsumptionBundles(appConfig),
70
+ packages: packages,
71
+ description: appConfig.env?.description || defaults.description,
72
+ openResourceDiscovery: appConfig.env?.openResourceDiscovery || defaults.openResourceDiscovery,
73
+
74
+ // Conditionally added top-level properties
75
+ ...(!entityTypes.length ? {} : { entityTypes: entityTypes }),
76
+ ...(!apiResources.length ? {} : { apiResources: apiResources }),
77
+ ...(!eventResources.length ? {} : { eventResources: eventResources }),
78
+ ...(appConfig.existingProductORDId ? {} : { products: [products[0]] }),
79
+ ...(!appConfig.serviceNames.length ? {} : { groups: _getGroups(linkedCsn, appConfig) }),
80
+ ...(!integrationDependencies.length ? {} : { integrationDependencies: integrationDependencies }),
81
+ ...(!appConfig.env?.consumptionBundles?.length ? {} : { consumptionBundles: appConfig.env.consumptionBundles }),
288
82
  };
83
+ };
289
84
 
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) {
85
+ const _filterUnusedPackages = (ordDocument) => {
310
86
  if (!ordDocument.packages?.length) return [];
311
87
 
312
88
  const usedPackageIds = new Set();
@@ -322,52 +98,33 @@ function _filterUnusedPackages(ordDocument) {
322
98
  });
323
99
 
324
100
  return ordDocument.packages.filter((pkg) => usedPackageIds.has(pkg.ordId));
325
- }
101
+ };
326
102
 
327
- module.exports = (csn, extensions = []) => {
328
- const linkedCsn = _propagateORDVisibility(cds.linked(csn));
329
- const appConfig = initializeAppConfig(linkedCsn);
103
+ const _getEntityTypes = (appConfig, packageIds) => {
104
+ return appConfig.entityTypeTargets.flatMap((entity) => createEntityTypeTemplate(appConfig, packageIds, entity));
105
+ };
330
106
 
331
- // Create auth config and fail-closed on configuration errors
107
+ const _createAuthConfig = () => {
332
108
  const authConfig = createAuthConfig();
109
+
110
+ // Create auth config and fail-closed on configuration errors
333
111
  if (authConfig.error) {
334
112
  throw new Error(`Authentication configuration error: ${authConfig.error}`);
335
113
  }
336
- const accessStrategies = authConfig.accessStrategies;
337
-
338
- let ordDocument = createDefaultORDDocument(linkedCsn, appConfig);
339
- const packageIds = extractPackageIds(ordDocument);
340
- const entityTypes = _getEntityTypes(appConfig, packageIds);
341
114
 
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
- }
115
+ return authConfig;
116
+ };
363
117
 
364
- [...(extensions || []), getCustomORDContent(appConfig)]
118
+ module.exports = (csn, extensions = []) => {
119
+ const authConfig = _createAuthConfig();
120
+ const linkedCsn = _propagateORDVisibility(cds.linked(csn));
121
+ const appConfig = new Configuration(linkedCsn);
122
+ const ordDocument = [...(extensions || []), getCustomORDContent(appConfig)]
365
123
  .filter((extension) => !!extension)
366
- .forEach((extension) => {
367
- ordDocument = compareAndHandleCustomORDContentWithExistingContent(ordDocument, extension);
368
- });
369
-
370
- ordDocument.packages = _filterUnusedPackages(ordDocument);
124
+ .reduce(
125
+ (document, extension) => compareAndHandleCustomORDContentWithExistingContent(document, extension),
126
+ _createDefaultORDDocument(linkedCsn, appConfig, authConfig),
127
+ );
371
128
 
372
- return ordDocument;
129
+ return Object.assign(ordDocument, { packages: _filterUnusedPackages(ordDocument) });
373
130
  };
@@ -7,18 +7,6 @@ const {
7
7
  } = require("./constants");
8
8
  const Logger = require("./logger");
9
9
 
10
- /**
11
- * Gets CAP endpoints for a service using CDS endpoints4().
12
- *
13
- * @param {string} serviceName The service name.
14
- * @param {Object} srvDefinition The service definition object.
15
- * @returns {Array} Raw endpoints from CDS.
16
- */
17
- function _getCapEndpoints(serviceName, srvDefinition) {
18
- const srvObj = { name: serviceName, definition: srvDefinition };
19
- return cds.service.protocols.endpoints4(srvObj);
20
- }
21
-
22
10
  /**
23
11
  * Reads the explicit @protocol annotation from service definition.
24
12
  *
@@ -42,13 +30,12 @@ function _getExplicitProtocol(srvDefinition) {
42
30
  * - Rule B: Only fallback to OData when no explicit protocol
43
31
  * - Rule C: Never produce [null] in entryPoints
44
32
  *
45
- * @param {string} serviceName The service name.
46
33
  * @param {Object} srvDefinition The service definition object.
47
34
  * @param {Object} options Configuration options.
48
35
  * @param {Function} options.isPrimaryDataProduct Strategy function to check if service is primary data product.
49
36
  * @returns {Array} Array with single {apiProtocol, entryPoints, hasResourceDefinitions} object, or empty array.
50
37
  */
51
- function resolveApiResourceProtocol(serviceName, srvDefinition, options = {}) {
38
+ function resolveApiResourceProtocol(srvDefinition, options = {}) {
52
39
  const { isPrimaryDataProduct = () => false } = options;
53
40
 
54
41
  // 1. Primary Data Product - early return
@@ -62,7 +49,7 @@ function resolveApiResourceProtocol(serviceName, srvDefinition, options = {}) {
62
49
  ];
63
50
  }
64
51
 
65
- const capEndpoints = _getCapEndpoints(serviceName, srvDefinition);
52
+ const capEndpoints = cds.service.protocols.endpoints4({ name: srvDefinition.name, definition: srvDefinition });
66
53
  const ordProtocols = [];
67
54
  for (const endpoint of capEndpoints) {
68
55
  if (PLUGIN_UNSUPPORTED_PROTOCOLS.includes(endpoint.kind)) {
@@ -107,7 +94,7 @@ function resolveApiResourceProtocol(serviceName, srvDefinition, options = {}) {
107
94
 
108
95
  // 4. Handle explicit protocol with no CAP endpoint (Rule A)
109
96
  if (!ordProtocols.some((p) => p.apiProtocol === protocol) && !CAP_TO_ORD_PROTOCOL_MAP[protocol]) {
110
- Logger.warn(`Unknown protocol '${protocol}' is not supported, and skipped for service '${serviceName}'.`);
97
+ Logger.warn(`Unknown protocol '${protocol}' is not supported, and skipped for service '${srvDefinition.name}'.`);
111
98
  }
112
99
  }
113
100
 
@@ -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 };