@sap-ux/axios-extension 1.22.3 → 1.22.4

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.
@@ -40,13 +40,20 @@ export declare abstract class CatalogService extends ODataService {
40
40
  entitySet: string;
41
41
  services: ODataServiceInfo[];
42
42
  isS4Cloud: Promise<boolean>;
43
- protected abstract fetchServices(): Promise<ODataServiceInfo[]>;
43
+ /**
44
+ * Fetch all services from the backend.
45
+ *
46
+ * @param useNextLink if true, the next link will be used to fetch the next page of results, pages are fetched serially.
47
+ */
48
+ protected abstract fetchServices(useNextLink?: boolean): Promise<ODataServiceInfo[]>;
44
49
  /**
45
50
  * Returns list of services from the catalog service.
46
51
  *
52
+ * @param useNextLink if true, the next link tags will be used to fetch the next page of results, pages are fetched serially.
53
+ * Note that this will be less performant for larger datasets.
47
54
  * @returns list of services
48
55
  */
49
- listServices(): Promise<ODataServiceInfo[]>;
56
+ listServices(useNextLink?: boolean): Promise<ODataServiceInfo[]>;
50
57
  abstract getAnnotations({ id, title, path }: FilterOptions): Promise<Annotations[]>;
51
58
  abstract getServiceType(path: string): Promise<ServiceType | undefined>;
52
59
  }
@@ -18,11 +18,13 @@ class CatalogService extends odata_service_1.ODataService {
18
18
  /**
19
19
  * Returns list of services from the catalog service.
20
20
  *
21
+ * @param useNextLink if true, the next link tags will be used to fetch the next page of results, pages are fetched serially.
22
+ * Note that this will be less performant for larger datasets.
21
23
  * @returns list of services
22
24
  */
23
- async listServices() {
25
+ async listServices(useNextLink = false) {
24
26
  if (!this.services) {
25
- this.services = await this.fetchServices();
27
+ this.services = await this.fetchServices(useNextLink);
26
28
  }
27
29
  return this.services;
28
30
  }
@@ -25,21 +25,37 @@ export interface ServiceGroup {
25
25
  * OData V4 specific implmentation of SAP's catalog service
26
26
  */
27
27
  export declare class V4CatalogService extends CatalogService {
28
+ private readonly logger;
28
29
  static readonly PATH = "/sap/opu/odata4/iwfnd/config/default/iwfnd/catalog/0002";
29
30
  /**
30
31
  * Map the V4 service information to a version independent structure.
31
32
  *
32
33
  * @param groups v4 service groups
33
- * @param entitySet entity set used for service selection
34
+ * @param entitySet entity set used for service selection. e.g. `RecommendedServices`
35
+ * @param dedup if true, duplicate services will be removed based on their id. Duplicate services may appear in multiple groups, e.g. '/IWBEP/ALL'.
34
36
  * @returns version independent information
35
37
  */
36
- protected mapServices(groups: ServiceGroup[], entitySet: string): ODataServiceInfo[];
38
+ protected mapServices(groups: ServiceGroup[], entitySet: string, dedup?: boolean): ODataServiceInfo[];
37
39
  /**
38
- * Fetch all services from the backend.
40
+ * Fetch all services from the backend using the @nexlink parameter to fetch all pages serially.
39
41
  *
40
42
  * @returns version independent service information
41
43
  */
42
- protected fetchServices(): Promise<ODataServiceInfo[]>;
44
+ protected fetchServicesNextLink(): Promise<ODataServiceInfo[]>;
45
+ /**
46
+ * Fetches all services from the catalog.
47
+ *
48
+ * @param useNextLink if true, uses the nextLink parameter to fetch all pages serially, otherwise fetches all pages in parallel.
49
+ * @returns v4 services
50
+ */
51
+ protected fetchServices(useNextLink?: boolean): Promise<ODataServiceInfo[]>;
52
+ /**
53
+ * Fetches all services from the catalog in parallel. Uses the total service count to fetch all service group pages in parallel
54
+ * to improve performance where larger numbers of services and therefore pages are available.
55
+ *
56
+ * @returns List of unique services
57
+ */
58
+ protected fetchServicesParallel(): Promise<ODataServiceInfo[]>;
43
59
  /**
44
60
  * For OData v4, all annotations are already included in the metadata and no additional request is required.
45
61
  *
@@ -4,26 +4,37 @@ exports.V4CatalogService = void 0;
4
4
  const base_1 = require("./base");
5
5
  const odata_service_1 = require("../../base/odata-service");
6
6
  const odata_request_error_1 = require("../../base/odata-request-error");
7
+ const logger_1 = require("@sap-ux/logger");
7
8
  const V4_RECOMMENDED_ENTITYSET = 'RecommendedServices';
8
9
  const V4_CLASSIC_ENTITYSET = 'Services';
9
10
  /**
10
11
  * OData V4 specific implmentation of SAP's catalog service
11
12
  */
12
13
  class V4CatalogService extends base_1.CatalogService {
14
+ logger = new logger_1.ToolsLogger();
13
15
  static PATH = '/sap/opu/odata4/iwfnd/config/default/iwfnd/catalog/0002';
14
16
  /**
15
17
  * Map the V4 service information to a version independent structure.
16
18
  *
17
19
  * @param groups v4 service groups
18
- * @param entitySet entity set used for service selection
20
+ * @param entitySet entity set used for service selection. e.g. `RecommendedServices`
21
+ * @param dedup if true, duplicate services will be removed based on their id. Duplicate services may appear in multiple groups, e.g. '/IWBEP/ALL'.
19
22
  * @returns version independent information
20
23
  */
21
- mapServices(groups, entitySet) {
24
+ mapServices(groups, entitySet, dedup = false) {
22
25
  const services = [];
26
+ // Duplicates can appear in multiple groups, e.g. '/IWBEP/ALL'
27
+ const uniqueServiceIds = new Set();
23
28
  groups
24
29
  .filter((group) => group?.DefaultSystem?.[entitySet]?.length > 0)
25
30
  .forEach((group) => {
26
- services.push(...group.DefaultSystem[entitySet].map((service) => {
31
+ services.push(...group.DefaultSystem[entitySet].flatMap((service) => {
32
+ if (dedup) {
33
+ if (uniqueServiceIds.has(service.ServiceId)) {
34
+ return [];
35
+ }
36
+ uniqueServiceIds.add(service.ServiceId);
37
+ }
27
38
  return {
28
39
  id: service.ServiceId,
29
40
  group: group.GroupId,
@@ -38,11 +49,11 @@ class V4CatalogService extends base_1.CatalogService {
38
49
  return services;
39
50
  }
40
51
  /**
41
- * Fetch all services from the backend.
52
+ * Fetch all services from the backend using the @nexlink parameter to fetch all pages serially.
42
53
  *
43
54
  * @returns version independent service information
44
55
  */
45
- async fetchServices() {
56
+ async fetchServicesNextLink() {
46
57
  if (this.entitySet === undefined) {
47
58
  const metadata = await this.metadata();
48
59
  this.entitySet = metadata.includes('Name="RecommendedServices"')
@@ -56,18 +67,85 @@ class V4CatalogService extends base_1.CatalogService {
56
67
  const response = await this.get('/ServiceGroups', { params }, true);
57
68
  let serviceGroupResponseOdata = response.odata();
58
69
  const serviceGroups = serviceGroupResponseOdata.value;
70
+ let numPageRequests = 1;
59
71
  // Page by using the backends nextLink search parameters for the next request
60
72
  while (serviceGroupResponseOdata['@odata.nextLink']) {
61
73
  const nextLink = new URL(serviceGroupResponseOdata['@odata.nextLink'], this.defaults.baseURL);
62
74
  serviceGroupResponseOdata = (await this.get('/ServiceGroups', { params: nextLink.searchParams }, true)).odata();
75
+ numPageRequests++;
63
76
  serviceGroups.push(...serviceGroupResponseOdata.value);
64
77
  }
78
+ this.logger.log(`Fetched ${serviceGroups.length} service groups in ${numPageRequests} requests.`);
65
79
  // check if the service responded with an odata error
66
80
  if (odata_request_error_1.ODataRequestError.containsError(serviceGroups)) {
67
81
  throw new odata_request_error_1.ODataRequestError(serviceGroups);
68
82
  }
69
83
  return this.mapServices(serviceGroups, this.entitySet);
70
84
  }
85
+ /**
86
+ * Fetches all services from the catalog.
87
+ *
88
+ * @param useNextLink if true, uses the nextLink parameter to fetch all pages serially, otherwise fetches all pages in parallel.
89
+ * @returns v4 services
90
+ */
91
+ async fetchServices(useNextLink = false) {
92
+ if (useNextLink) {
93
+ return this.fetchServicesNextLink();
94
+ }
95
+ return this.fetchServicesParallel();
96
+ }
97
+ /**
98
+ * Fetches all services from the catalog in parallel. Uses the total service count to fetch all service group pages in parallel
99
+ * to improve performance where larger numbers of services and therefore pages are available.
100
+ *
101
+ * @returns List of unique services
102
+ */
103
+ async fetchServicesParallel() {
104
+ const defaultInitialPageSize = 1000; // default page size for the services, large enough to get the first page and skiptoken to determine max page size
105
+ if (this.entitySet === undefined) {
106
+ const metadata = await this.metadata();
107
+ this.entitySet = metadata.includes('Name="RecommendedServices"')
108
+ ? V4_RECOMMENDED_ENTITYSET
109
+ : V4_CLASSIC_ENTITYSET;
110
+ }
111
+ const params = new URLSearchParams([
112
+ ['$count', 'true'],
113
+ ['$top', defaultInitialPageSize.toString()], // Get the first page of services,
114
+ ['$expand', `DefaultSystem($expand=${this.entitySet})`]
115
+ ]);
116
+ const response = await this.get('/ServiceGroups', { params }, true);
117
+ const serviceGroupResponseOdata = response.odata();
118
+ const serviceGroups = serviceGroupResponseOdata.value;
119
+ const serviceGroupCount = serviceGroupResponseOdata['@odata.count'];
120
+ const pageSize = parseInt(serviceGroupResponseOdata['@odata.nextLink']?.split('skiptoken=')[1], 10);
121
+ let numPageRequests = 1;
122
+ // If we dont have a valid skip token, we assume we have all services in the first page
123
+ if (!isNaN(pageSize)) {
124
+ const numPages = Math.ceil(serviceGroupCount / pageSize);
125
+ // Create an array of promises to fetch all pages in parallel
126
+ const fetchPromises = Array.from({ length: numPages - 1 }, (_, index) => {
127
+ const nextParams = new URLSearchParams([
128
+ ['$count', 'true'],
129
+ ['$skip', String((index + 1) * pageSize)],
130
+ ['$top', pageSize.toString()], // Fetch the next 200 services
131
+ ['$expand', `DefaultSystem($expand=${this.entitySet})`]
132
+ ]);
133
+ numPageRequests++;
134
+ return this.get('/ServiceGroups', { params: nextParams }, true);
135
+ });
136
+ const pageResults = await Promise.all(fetchPromises); // Fetch all remaining pages in parallel
137
+ pageResults.forEach((pageResponse) => {
138
+ const pageData = pageResponse.odata();
139
+ serviceGroups.push(...pageData.value);
140
+ });
141
+ }
142
+ this.logger.log(`Fetched ${serviceGroups.length} service groups in ${numPageRequests} requests.`);
143
+ // check if the service responded with an odata error
144
+ if (odata_request_error_1.ODataRequestError.containsError(serviceGroups)) {
145
+ throw new odata_request_error_1.ODataRequestError(serviceGroups);
146
+ }
147
+ return this.mapServices(serviceGroups, this.entitySet, true);
148
+ }
71
149
  /**
72
150
  * For OData v4, all annotations are already included in the metadata and no additional request is required.
73
151
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap-ux/axios-extension",
3
- "version": "1.22.3",
3
+ "version": "1.22.4",
4
4
  "description": "Extension of the Axios module adding convenience methods to interact with SAP systems especially with OData services.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "nock": "13.4.0",
35
35
  "supertest": "6.3.3",
36
36
  "@types/proxy-from-env": "1.0.1",
37
- "@sap-ux/project-access": "1.30.3"
37
+ "@sap-ux/project-access": "1.30.7"
38
38
  },
39
39
  "files": [
40
40
  "dist",