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