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