xref: /openbmc/webui-vue/src/api/composables/useRedfishCollection.ts (revision d3b05033fe82aed6c53f4f2b52c23d3bd285d423)
1*d3b05033SNishant Tiwariimport { useQuery } from '@tanstack/vue-query';
2*d3b05033SNishant Tiwariimport { computed } from 'vue';
3*d3b05033SNishant Tiwariimport api from '@/store/api';
4*d3b05033SNishant Tiwariimport { useRedfishRoot, supportsExpandQuery } from './useRedfishRoot';
5*d3b05033SNishant Tiwari
6*d3b05033SNishant Tiwari/**
7*d3b05033SNishant Tiwari * Redfish collection member reference
8*d3b05033SNishant Tiwari */
9*d3b05033SNishant Tiwariexport interface CollectionMember {
10*d3b05033SNishant Tiwari  '@odata.id': string;
11*d3b05033SNishant Tiwari}
12*d3b05033SNishant Tiwari
13*d3b05033SNishant Tiwari/**
14*d3b05033SNishant Tiwari * Redfish collection response
15*d3b05033SNishant Tiwari */
16*d3b05033SNishant Tiwariexport interface RedfishCollection<T = unknown> {
17*d3b05033SNishant Tiwari  '@odata.id': string;
18*d3b05033SNishant Tiwari  '@odata.type': string;
19*d3b05033SNishant Tiwari  Name: string;
20*d3b05033SNishant Tiwari  Members: T[];
21*d3b05033SNishant Tiwari  'Members@odata.count': number;
22*d3b05033SNishant Tiwari}
23*d3b05033SNishant Tiwari
24*d3b05033SNishant Tiwari/**
25*d3b05033SNishant Tiwari * OData Query Parameters for Redfish API
26*d3b05033SNishant Tiwari */
27*d3b05033SNishant Tiwariexport interface RedfishQueryParameters {
28*d3b05033SNishant Tiwari  $expand?:
29*d3b05033SNishant Tiwari    | string
30*d3b05033SNishant Tiwari    | {
31*d3b05033SNishant Tiwari        $levels?: number;
32*d3b05033SNishant Tiwari        $noLinks?: boolean;
33*d3b05033SNishant Tiwari        $expandAll?: boolean;
34*d3b05033SNishant Tiwari        $links?: string;
35*d3b05033SNishant Tiwari      };
36*d3b05033SNishant Tiwari  $filter?: string;
37*d3b05033SNishant Tiwari  $select?: string | string[];
38*d3b05033SNishant Tiwari  $top?: number;
39*d3b05033SNishant Tiwari  $skip?: number;
40*d3b05033SNishant Tiwari  only?: boolean;
41*d3b05033SNishant Tiwari  excerpt?: boolean;
42*d3b05033SNishant Tiwari}
43*d3b05033SNishant Tiwari
44*d3b05033SNishant Tiwari/**
45*d3b05033SNishant Tiwari * Options for fetching a Redfish collection
46*d3b05033SNishant Tiwari */
47*d3b05033SNishant Tiwariexport interface FetchCollectionOptions {
48*d3b05033SNishant Tiwari  expand?: boolean;
49*d3b05033SNishant Tiwari  expandLevels?: number;
50*d3b05033SNishant Tiwari  select?: string[];
51*d3b05033SNishant Tiwari  filter?: string;
52*d3b05033SNishant Tiwari}
53*d3b05033SNishant Tiwari
54*d3b05033SNishant Tiwari/**
55*d3b05033SNishant Tiwari * Builds a Redfish API URL with OData query parameters
56*d3b05033SNishant Tiwari *
57*d3b05033SNishant Tiwari * Handles proper encoding and formatting of OData directives:
58*d3b05033SNishant Tiwari * - $expand with nested options like .($levels=2)
59*d3b05033SNishant Tiwari * - $select with multiple properties
60*d3b05033SNishant Tiwari * - $filter, $top, $skip for pagination and filtering
61*d3b05033SNishant Tiwari * - Custom Redfish parameters like 'only' and 'excerpt'
62*d3b05033SNishant Tiwari *
63*d3b05033SNishant Tiwari * @param path - Base path (e.g., '/redfish/v1/Chassis')
64*d3b05033SNishant Tiwari * @param params - OData query parameters
65*d3b05033SNishant Tiwari * @returns Complete URL with query string
66*d3b05033SNishant Tiwari *
67*d3b05033SNishant Tiwari * @example
68*d3b05033SNishant Tiwari * buildQuery('/redfish/v1/Chassis', { $expand: '*' })
69*d3b05033SNishant Tiwari * // Returns: '/redfish/v1/Chassis?$expand=*'
70*d3b05033SNishant Tiwari *
71*d3b05033SNishant Tiwari * @example
72*d3b05033SNishant Tiwari * buildQuery('/redfish/v1/Systems', {
73*d3b05033SNishant Tiwari *   $expand: { $levels: 2, $noLinks: true }
74*d3b05033SNishant Tiwari * })
75*d3b05033SNishant Tiwari * // Returns: '/redfish/v1/Systems?$expand=.($levels=2;$noLinks=true)'
76*d3b05033SNishant Tiwari */
77*d3b05033SNishant Tiwariexport function buildQuery(
78*d3b05033SNishant Tiwari  path: string,
79*d3b05033SNishant Tiwari  params?: RedfishQueryParameters,
80*d3b05033SNishant Tiwari): string {
81*d3b05033SNishant Tiwari  if (!params) return path;
82*d3b05033SNishant Tiwari
83*d3b05033SNishant Tiwari  const pairs: string[] = [];
84*d3b05033SNishant Tiwari
85*d3b05033SNishant Tiwari  // Handle $expand parameter
86*d3b05033SNishant Tiwari  if (params.$expand) {
87*d3b05033SNishant Tiwari    if (typeof params.$expand === 'string') {
88*d3b05033SNishant Tiwari      // Simple string expand (e.g., '*' or 'Members')
89*d3b05033SNishant Tiwari      // Do not encode $ directives inside the value
90*d3b05033SNishant Tiwari      pairs.push(`$expand=${params.$expand}`);
91*d3b05033SNishant Tiwari    } else {
92*d3b05033SNishant Tiwari      // Complex expand with options
93*d3b05033SNishant Tiwari      const expandParts: string[] = [];
94*d3b05033SNishant Tiwari
95*d3b05033SNishant Tiwari      if (params.$expand.$levels !== undefined) {
96*d3b05033SNishant Tiwari        expandParts.push(`$levels=${params.$expand.$levels}`);
97*d3b05033SNishant Tiwari      }
98*d3b05033SNishant Tiwari      if (params.$expand.$noLinks !== undefined) {
99*d3b05033SNishant Tiwari        expandParts.push(`$noLinks=${params.$expand.$noLinks}`);
100*d3b05033SNishant Tiwari      }
101*d3b05033SNishant Tiwari      if (params.$expand.$expandAll !== undefined) {
102*d3b05033SNishant Tiwari        expandParts.push(`$expandAll=${params.$expand.$expandAll}`);
103*d3b05033SNishant Tiwari      }
104*d3b05033SNishant Tiwari      if (params.$expand.$links !== undefined) {
105*d3b05033SNishant Tiwari        expandParts.push(`$links=${params.$expand.$links}`);
106*d3b05033SNishant Tiwari      }
107*d3b05033SNishant Tiwari
108*d3b05033SNishant Tiwari      // Build .(options) without encoding the $ directives
109*d3b05033SNishant Tiwari      // Use ';' between options per OData specification
110*d3b05033SNishant Tiwari      const opts = expandParts.join(';');
111*d3b05033SNishant Tiwari      pairs.push(`$expand=.(${opts})`);
112*d3b05033SNishant Tiwari    }
113*d3b05033SNishant Tiwari  }
114*d3b05033SNishant Tiwari
115*d3b05033SNishant Tiwari  // Handle $filter parameter
116*d3b05033SNishant Tiwari  if (params.$filter) {
117*d3b05033SNishant Tiwari    pairs.push(`$filter=${encodeURIComponent(params.$filter)}`);
118*d3b05033SNishant Tiwari  }
119*d3b05033SNishant Tiwari
120*d3b05033SNishant Tiwari  // Handle $select parameter
121*d3b05033SNishant Tiwari  if (params.$select) {
122*d3b05033SNishant Tiwari    const sel = Array.isArray(params.$select)
123*d3b05033SNishant Tiwari      ? params.$select.join(',')
124*d3b05033SNishant Tiwari      : params.$select;
125*d3b05033SNishant Tiwari    pairs.push(`$select=${encodeURIComponent(sel)}`);
126*d3b05033SNishant Tiwari  }
127*d3b05033SNishant Tiwari
128*d3b05033SNishant Tiwari  // Handle $top parameter (pagination)
129*d3b05033SNishant Tiwari  if (params.$top !== undefined) {
130*d3b05033SNishant Tiwari    pairs.push(`$top=${encodeURIComponent(String(params.$top))}`);
131*d3b05033SNishant Tiwari  }
132*d3b05033SNishant Tiwari
133*d3b05033SNishant Tiwari  // Handle $skip parameter (pagination)
134*d3b05033SNishant Tiwari  if (params.$skip !== undefined) {
135*d3b05033SNishant Tiwari    pairs.push(`$skip=${encodeURIComponent(String(params.$skip))}`);
136*d3b05033SNishant Tiwari  }
137*d3b05033SNishant Tiwari
138*d3b05033SNishant Tiwari  // Handle 'only' parameter (Redfish-specific)
139*d3b05033SNishant Tiwari  if (params.only) {
140*d3b05033SNishant Tiwari    pairs.push('only=');
141*d3b05033SNishant Tiwari  }
142*d3b05033SNishant Tiwari
143*d3b05033SNishant Tiwari  // Handle 'excerpt' parameter (Redfish-specific)
144*d3b05033SNishant Tiwari  if (params.excerpt !== undefined) {
145*d3b05033SNishant Tiwari    pairs.push(`excerpt=${encodeURIComponent(String(params.excerpt))}`);
146*d3b05033SNishant Tiwari  }
147*d3b05033SNishant Tiwari
148*d3b05033SNishant Tiwari  const qs = pairs.join('&');
149*d3b05033SNishant Tiwari  return qs ? `${path}?${qs}` : path;
150*d3b05033SNishant Tiwari}
151*d3b05033SNishant Tiwari
152*d3b05033SNishant Tiwari/**
153*d3b05033SNishant Tiwari * Normalizes Redfish query parameters for cache stability
154*d3b05033SNishant Tiwari *
155*d3b05033SNishant Tiwari * Ensures consistent query keys by:
156*d3b05033SNishant Tiwari * - Sorting array values (like $select)
157*d3b05033SNishant Tiwari * - Freezing the result to prevent mutations
158*d3b05033SNishant Tiwari * - Handling undefined values consistently
159*d3b05033SNishant Tiwari *
160*d3b05033SNishant Tiwari * @param params - Query parameters to normalize
161*d3b05033SNishant Tiwari * @returns Normalized and frozen parameters, or undefined if input is undefined
162*d3b05033SNishant Tiwari */
163*d3b05033SNishant Tiwarifunction normalizeRedfishQueryParameters(
164*d3b05033SNishant Tiwari  params?: RedfishQueryParameters,
165*d3b05033SNishant Tiwari): Readonly<RedfishQueryParameters> | undefined {
166*d3b05033SNishant Tiwari  if (!params) return undefined;
167*d3b05033SNishant Tiwari
168*d3b05033SNishant Tiwari  const normalizedSelect =
169*d3b05033SNishant Tiwari    params.$select === undefined
170*d3b05033SNishant Tiwari      ? undefined
171*d3b05033SNishant Tiwari      : Array.isArray(params.$select)
172*d3b05033SNishant Tiwari        ? [...params.$select].sort()
173*d3b05033SNishant Tiwari        : params.$select;
174*d3b05033SNishant Tiwari
175*d3b05033SNishant Tiwari  const normalizedExpand =
176*d3b05033SNishant Tiwari    params.$expand === undefined
177*d3b05033SNishant Tiwari      ? undefined
178*d3b05033SNishant Tiwari      : typeof params.$expand === 'string'
179*d3b05033SNishant Tiwari        ? params.$expand
180*d3b05033SNishant Tiwari        : {
181*d3b05033SNishant Tiwari            $levels: params.$expand.$levels,
182*d3b05033SNishant Tiwari            $noLinks: params.$expand.$noLinks,
183*d3b05033SNishant Tiwari            $expandAll: params.$expand.$expandAll,
184*d3b05033SNishant Tiwari            $links: params.$expand.$links,
185*d3b05033SNishant Tiwari          };
186*d3b05033SNishant Tiwari
187*d3b05033SNishant Tiwari  return Object.freeze({
188*d3b05033SNishant Tiwari    $expand: normalizedExpand,
189*d3b05033SNishant Tiwari    $filter: params.$filter,
190*d3b05033SNishant Tiwari    $select: normalizedSelect,
191*d3b05033SNishant Tiwari    $top: params.$top,
192*d3b05033SNishant Tiwari    $skip: params.$skip,
193*d3b05033SNishant Tiwari    only: params.only,
194*d3b05033SNishant Tiwari    excerpt: params.excerpt,
195*d3b05033SNishant Tiwari  });
196*d3b05033SNishant Tiwari}
197*d3b05033SNishant Tiwari
198*d3b05033SNishant Tiwari/**
199*d3b05033SNishant Tiwari * Fetches a Redfish collection with optional OData query parameters
200*d3b05033SNishant Tiwari * Gracefully falls back if BMC doesn't support OData features
201*d3b05033SNishant Tiwari *
202*d3b05033SNishant Tiwari * @param path - Collection path (e.g., '/redfish/v1/Chassis')
203*d3b05033SNishant Tiwari * @param options - Fetch options
204*d3b05033SNishant Tiwari * @param supportsExpand - Whether BMC supports $expand
205*d3b05033SNishant Tiwari * @returns Promise with collection data
206*d3b05033SNishant Tiwari */
207*d3b05033SNishant Tiwariasync function fetchCollection<T>(
208*d3b05033SNishant Tiwari  path: string,
209*d3b05033SNishant Tiwari  options: FetchCollectionOptions,
210*d3b05033SNishant Tiwari  supportsExpand: boolean,
211*d3b05033SNishant Tiwari): Promise<T[]> {
212*d3b05033SNishant Tiwari  const { expand, expandLevels = 1, select, filter } = options;
213*d3b05033SNishant Tiwari
214*d3b05033SNishant Tiwari  // Build query parameters using the reusable buildQuery function
215*d3b05033SNishant Tiwari  const queryParams: RedfishQueryParameters = {};
216*d3b05033SNishant Tiwari
217*d3b05033SNishant Tiwari  if (expand && supportsExpand) {
218*d3b05033SNishant Tiwari    queryParams.$expand = { $levels: expandLevels };
219*d3b05033SNishant Tiwari  }
220*d3b05033SNishant Tiwari
221*d3b05033SNishant Tiwari  if (select && select.length > 0) {
222*d3b05033SNishant Tiwari    queryParams.$select = select;
223*d3b05033SNishant Tiwari  }
224*d3b05033SNishant Tiwari
225*d3b05033SNishant Tiwari  if (filter) {
226*d3b05033SNishant Tiwari    queryParams.$filter = filter;
227*d3b05033SNishant Tiwari  }
228*d3b05033SNishant Tiwari
229*d3b05033SNishant Tiwari  const url = buildQuery(path, queryParams);
230*d3b05033SNishant Tiwari
231*d3b05033SNishant Tiwari  try {
232*d3b05033SNishant Tiwari    const { data } = await api.get<RedfishCollection<T>>(url);
233*d3b05033SNishant Tiwari
234*d3b05033SNishant Tiwari    if (expand && supportsExpand && data.Members) {
235*d3b05033SNishant Tiwari      return data.Members;
236*d3b05033SNishant Tiwari    }
237*d3b05033SNishant Tiwari
238*d3b05033SNishant Tiwari    if (data.Members && Array.isArray(data.Members)) {
239*d3b05033SNishant Tiwari      const memberPromises = data.Members.map((member: CollectionMember) =>
240*d3b05033SNishant Tiwari        api
241*d3b05033SNishant Tiwari          .get<T>(member['@odata.id'])
242*d3b05033SNishant Tiwari          .then((res: { data: T }) => res.data)
243*d3b05033SNishant Tiwari          .catch((error: Object) => {
244*d3b05033SNishant Tiwari            console.error(
245*d3b05033SNishant Tiwari              `Error fetching member ${member['@odata.id']}:`,
246*d3b05033SNishant Tiwari              error,
247*d3b05033SNishant Tiwari            );
248*d3b05033SNishant Tiwari            return null;
249*d3b05033SNishant Tiwari          }),
250*d3b05033SNishant Tiwari      );
251*d3b05033SNishant Tiwari
252*d3b05033SNishant Tiwari      const members = await Promise.all(memberPromises);
253*d3b05033SNishant Tiwari      return members.filter((m: T | null): m is T => m !== null);
254*d3b05033SNishant Tiwari    }
255*d3b05033SNishant Tiwari
256*d3b05033SNishant Tiwari    return [];
257*d3b05033SNishant Tiwari  } catch (error) {
258*d3b05033SNishant Tiwari    // If OData query failed, try without parameters
259*d3b05033SNishant Tiwari    const hasQueryParams = url !== path;
260*d3b05033SNishant Tiwari    if (hasQueryParams) {
261*d3b05033SNishant Tiwari      console.warn(
262*d3b05033SNishant Tiwari        `OData query failed for ${path}, falling back to basic fetch`,
263*d3b05033SNishant Tiwari      );
264*d3b05033SNishant Tiwari      try {
265*d3b05033SNishant Tiwari        const { data } =
266*d3b05033SNishant Tiwari          await api.get<RedfishCollection<CollectionMember>>(path);
267*d3b05033SNishant Tiwari
268*d3b05033SNishant Tiwari        if (data.Members && Array.isArray(data.Members)) {
269*d3b05033SNishant Tiwari          const memberPromises = data.Members.map((member: CollectionMember) =>
270*d3b05033SNishant Tiwari            api
271*d3b05033SNishant Tiwari              .get<T>(member['@odata.id'])
272*d3b05033SNishant Tiwari              .then((res: { data: T }) => res.data)
273*d3b05033SNishant Tiwari              .catch((err: Object) => {
274*d3b05033SNishant Tiwari                console.error(
275*d3b05033SNishant Tiwari                  `Error fetching member ${member['@odata.id']}:`,
276*d3b05033SNishant Tiwari                  err,
277*d3b05033SNishant Tiwari                );
278*d3b05033SNishant Tiwari                return null;
279*d3b05033SNishant Tiwari              }),
280*d3b05033SNishant Tiwari          );
281*d3b05033SNishant Tiwari
282*d3b05033SNishant Tiwari          const members = await Promise.all(memberPromises);
283*d3b05033SNishant Tiwari          return members.filter((m: T | null): m is T => m !== null);
284*d3b05033SNishant Tiwari        }
285*d3b05033SNishant Tiwari      } catch (fallbackError) {
286*d3b05033SNishant Tiwari        console.error(`Failed to fetch collection ${path}:`, fallbackError);
287*d3b05033SNishant Tiwari        throw fallbackError;
288*d3b05033SNishant Tiwari      }
289*d3b05033SNishant Tiwari    }
290*d3b05033SNishant Tiwari
291*d3b05033SNishant Tiwari    console.error(`Failed to fetch collection ${path}:`, error);
292*d3b05033SNishant Tiwari    throw error;
293*d3b05033SNishant Tiwari  }
294*d3b05033SNishant Tiwari}
295*d3b05033SNishant Tiwari
296*d3b05033SNishant Tiwari/**
297*d3b05033SNishant Tiwari * TanStack Query hook for fetching a Redfish collection
298*d3b05033SNishant Tiwari *
299*d3b05033SNishant Tiwari * @param path - Collection path
300*d3b05033SNishant Tiwari * @param options - Fetch options
301*d3b05033SNishant Tiwari * @returns TanStack Query result
302*d3b05033SNishant Tiwari */
303*d3b05033SNishant Tiwariexport function useRedfishCollection<T>(
304*d3b05033SNishant Tiwari  path: string,
305*d3b05033SNishant Tiwari  options: FetchCollectionOptions = {},
306*d3b05033SNishant Tiwari) {
307*d3b05033SNishant Tiwari  // Get ServiceRoot to check OData support
308*d3b05033SNishant Tiwari  const { data: serviceRoot } = useRedfishRoot();
309*d3b05033SNishant Tiwari
310*d3b05033SNishant Tiwari  // Compute whether expand is supported
311*d3b05033SNishant Tiwari  const canExpand = computed(() => supportsExpandQuery(serviceRoot.value));
312*d3b05033SNishant Tiwari
313*d3b05033SNishant Tiwari  // Build query parameters for normalization
314*d3b05033SNishant Tiwari  const queryParams: RedfishQueryParameters = {};
315*d3b05033SNishant Tiwari
316*d3b05033SNishant Tiwari  if (options.expand) {
317*d3b05033SNishant Tiwari    queryParams.$expand = { $levels: options.expandLevels || 1 };
318*d3b05033SNishant Tiwari  }
319*d3b05033SNishant Tiwari
320*d3b05033SNishant Tiwari  if (options.select && options.select.length > 0) {
321*d3b05033SNishant Tiwari    queryParams.$select = options.select;
322*d3b05033SNishant Tiwari  }
323*d3b05033SNishant Tiwari
324*d3b05033SNishant Tiwari  if (options.filter) {
325*d3b05033SNishant Tiwari    queryParams.$filter = options.filter;
326*d3b05033SNishant Tiwari  }
327*d3b05033SNishant Tiwari
328*d3b05033SNishant Tiwari  // Normalize query parameters for stable cache keys
329*d3b05033SNishant Tiwari  const normalizedParams = normalizeRedfishQueryParameters(queryParams);
330*d3b05033SNishant Tiwari
331*d3b05033SNishant Tiwari  return useQuery({
332*d3b05033SNishant Tiwari    queryKey: ['redfish', 'collection', path, normalizedParams],
333*d3b05033SNishant Tiwari    queryFn: () => fetchCollection<T>(path, options, canExpand.value),
334*d3b05033SNishant Tiwari    enabled: computed(() => !!serviceRoot.value),
335*d3b05033SNishant Tiwari    refetchOnMount: false, // Don't refetch when component remounts
336*d3b05033SNishant Tiwari    refetchOnWindowFocus: false, // Don't refetch when window regains focus
337*d3b05033SNishant Tiwari    refetchOnReconnect: false,
338*d3b05033SNishant Tiwari    retry: 2,
339*d3b05033SNishant Tiwari    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
340*d3b05033SNishant Tiwari  });
341*d3b05033SNishant Tiwari}
342