1import { useQuery, useQueryClient } from '@tanstack/vue-query'; 2import { computed } from 'vue'; 3import api from '@/store/api'; 4import { 5 useRedfishRoot, 6 supportsExpandQuery, 7 supportsSelectQuery, 8} from './useRedfishRoot'; 9import type { CollectionMember } from './useRedfishCollection'; 10 11/** 12 * Generic Redfish resource with sub-resource collection 13 */ 14interface ResourceWithCollection { 15 '@odata.id': string; 16 [key: string]: unknown; 17} 18 19/** 20 * Fetches all parent resources and checks which have the sub-resource 21 * Uses $select for efficiency if supported 22 * 23 * @param parentCollectionPath - Path to parent collection (e.g., '/redfish/v1/Chassis') 24 * @param subResourceName - Name of sub-resource (e.g., 'Sensors') 25 * @param canUseSelect - Whether BMC supports $select 26 * @returns Array of parent resource URIs that have the sub-resource 27 */ 28async function discoverParentsWithSubResource( 29 parentCollectionPath: string, 30 subResourceName: string, 31 canUseSelect: boolean, 32): Promise<string[]> { 33 try { 34 const selectParam = canUseSelect ? `?$select=${subResourceName}` : ''; 35 const { data } = await api.get(`${parentCollectionPath}${selectParam}`); 36 37 if (!data.Members || !Array.isArray(data.Members)) { 38 // Check if this is a single resource with the sub-resource 39 if (data['@odata.id'] && data[subResourceName]) { 40 return [data['@odata.id']]; 41 } 42 return []; 43 } 44 45 if ( 46 canUseSelect && 47 data.Members.length > 0 && 48 data.Members[0][subResourceName] 49 ) { 50 return data.Members.filter( 51 (member: ResourceWithCollection) => member[subResourceName], 52 ).map((member: ResourceWithCollection) => member['@odata.id']); 53 } 54 55 const checkPromises = data.Members.map(async (member: CollectionMember) => { 56 try { 57 const { data: parentData } = await api.get<ResourceWithCollection>( 58 member['@odata.id'], 59 ); 60 return parentData[subResourceName] ? member['@odata.id'] : null; 61 } catch (error) { 62 console.error( 63 `Error checking ${member['@odata.id']} for ${subResourceName}:`, 64 error, 65 ); 66 return null; 67 } 68 }); 69 70 const results = await Promise.all(checkPromises); 71 const foundParents = results.filter( 72 (uri: String): uri is string => uri !== null, 73 ); 74 return foundParents; 75 } catch (error) { 76 console.error(`Error discovering parents with ${subResourceName}:`, error); 77 return []; 78 } 79} 80 81/** 82 * Fetches sub-resources from a single parent 83 * Uses $expand if supported, falls back to individual fetches 84 * 85 * @param parentUri - Parent resource URI 86 * @param subResourceName - Name of sub-resource collection 87 * @param canExpand - Whether BMC supports $expand 88 * @param queryClient - TanStack Query client for incremental updates 89 * @param queryKey - Query key for cache updates 90 * @returns Array of sub-resources 91 */ 92async function fetchSubResourcesFromParent<T>( 93 parentUri: string, 94 subResourceName: string, 95 canExpand: boolean, 96 queryClient: ReturnType<typeof useQueryClient>, 97 queryKey: unknown[], 98): Promise<T[]> { 99 const subResourcePath = `${parentUri}/${subResourceName}`; 100 101 try { 102 if (canExpand) { 103 const { data } = await api.get(`${subResourcePath}?$expand=.($levels=1)`); 104 105 if (data.Members && Array.isArray(data.Members)) { 106 queryClient.setQueryData(queryKey, (oldData: T[] = []) => [ 107 ...oldData, 108 ...data.Members, 109 ]); 110 return data.Members; 111 } 112 } 113 114 const { data: collection } = await api.get(subResourcePath); 115 116 if (!collection.Members || !Array.isArray(collection.Members)) { 117 return []; 118 } 119 120 const memberPromises = collection.Members.map( 121 async (member: CollectionMember) => { 122 try { 123 const { data: memberData } = await api.get<T>(member['@odata.id']); 124 queryClient.setQueryData(queryKey, (oldData: T[] = []) => [ 125 ...oldData, 126 memberData, 127 ]); 128 return memberData; 129 } catch (error) { 130 console.error(`Error fetching ${member['@odata.id']}:`, error); 131 return null; 132 } 133 }, 134 ); 135 136 const members = await Promise.all(memberPromises); 137 return members.filter((m: T | null): m is T => m !== null); 138 } catch (error) { 139 // Silently handle 404 - sub-resource may not exist on this parent 140 if ( 141 (error as { response?: { status?: number } }).response?.status !== 404 142 ) { 143 console.error( 144 `Error fetching ${subResourceName} from ${parentUri}:`, 145 error, 146 ); 147 } 148 return []; 149 } 150} 151 152/** 153 * Deduplicates resources by @odata.id 154 * Prevents duplicate entries when the same resource is referenced by multiple parents 155 * 156 * @param items - Array of resources to deduplicate 157 * @returns Deduplicated array 158 */ 159function deduplicateByOdataId<T>(items: T[]): T[] { 160 if (items.length === 0) return items; 161 162 const seen = new Set<string>(); 163 const deduplicated: T[] = []; 164 165 for (const item of items) { 166 const id = (item as any)['@odata.id']; 167 if (id) { 168 if (seen.has(id)) continue; 169 seen.add(id); 170 } 171 deduplicated.push(item); 172 } 173 174 return deduplicated; 175} 176 177/** 178 * Fetches all sub-resources from all parent resources 179 * 180 * @param parentUris - Array of parent resource URIs 181 * @param subResourceName - Name of sub-resource collection 182 * @param canExpand - Whether BMC supports $expand 183 * @param queryClient - TanStack Query client 184 * @param queryKey - Query key for cache updates 185 * @returns Array of all sub-resources (deduplicated by @odata.id) 186 * @throws Error if all requests fail and no data is retrieved 187 */ 188async function fetchAllSubResources<T>( 189 parentUris: string[], 190 subResourceName: string, 191 canExpand: boolean, 192 queryClient: ReturnType<typeof useQueryClient>, 193 queryKey: unknown[], 194): Promise<T[]> { 195 queryClient.setQueryData(queryKey, []); 196 197 const fetchPromises = parentUris.map((parentUri) => 198 fetchSubResourcesFromParent<T>( 199 parentUri, 200 subResourceName, 201 canExpand, 202 queryClient, 203 queryKey, 204 ).catch((error) => { 205 // Return empty array on failure but track the error 206 console.error( 207 `Failed to fetch ${subResourceName} from ${parentUri}:`, 208 error, 209 ); 210 return [] as T[]; 211 }), 212 ); 213 214 const results = await Promise.all(fetchPromises); 215 const flattened = results.flat(); 216 217 // Track failed requests (those that returned empty arrays) 218 const failed = results.filter((result) => result.length === 0); 219 220 // If all requests failed and we have no data, throw an error 221 if (flattened.length === 0 && failed.length > 0) { 222 const error = new Error( 223 `Failed to fetch ${subResourceName} collections (${failed.length}/${results.length} requests failed).`, 224 ); 225 throw error; 226 } 227 228 // Deduplicate by @odata.id to prevent duplicate entries 229 // when the same resource is referenced by multiple parents 230 return deduplicateByOdataId(flattened); 231} 232 233/** 234 * Smart retry function that doesn't retry on 4xx client errors. 235 * 4xx errors (like 404 Not Found) won't succeed on retry. 236 * 237 * @param failureCount - Number of times the request has failed 238 * @param error - The error object from the failed request 239 * @returns Whether to retry the request 240 */ 241function shouldRetry(failureCount: number, error: unknown): boolean { 242 const status = (error as { response?: { status?: number } })?.response 243 ?.status; 244 // Don't retry on 4xx client errors (bad request, not found, unauthorized, etc.) 245 if (status && status >= 400 && status < 500) { 246 return false; 247 } 248 // Retry up to 3 times for other errors (network issues, 5xx server errors) 249 return failureCount < 3; 250} 251 252/** 253 * Generic composable for fetching all sub-resources from a collection 254 * 255 * @param parentCollectionPath - Path to parent collection 256 * @param subResourceName - Name of sub-resource collection 257 * @returns TanStack Query result with all sub-resources 258 */ 259export function useAllSubResources<T>( 260 parentCollectionPath: string, 261 subResourceName: string, 262) { 263 const queryClient = useQueryClient(); 264 const { data: serviceRoot } = useRedfishRoot(); 265 266 const canExpand = computed(() => supportsExpandQuery(serviceRoot.value)); 267 const canSelect = computed(() => supportsSelectQuery(serviceRoot.value)); 268 269 // Query key for this specific sub-resource fetch 270 const queryKey = [ 271 'redfish', 272 'allSubResources', 273 parentCollectionPath, 274 subResourceName, 275 ]; 276 277 return useQuery({ 278 queryKey, 279 queryFn: async () => { 280 // Always perform discovery on each query execution 281 // This ensures fresh data and proper cache behavior 282 const parentUris = await discoverParentsWithSubResource( 283 parentCollectionPath, 284 subResourceName, 285 canSelect.value, 286 ); 287 288 return fetchAllSubResources<T>( 289 parentUris, 290 subResourceName, 291 canExpand.value, 292 queryClient, 293 queryKey, 294 ); 295 }, 296 enabled: computed(() => !!serviceRoot.value), 297 staleTime: Infinity, // Data never becomes stale automatically 298 gcTime: 300000, // Keep in cache for 5 minutes after component unmount 299 refetchOnMount: false, // Don't refetch when component remounts 300 refetchOnWindowFocus: false, // Don't refetch when window regains focus 301 refetchOnReconnect: false, // Don't refetch on network reconnect 302 placeholderData: (prev) => prev, 303 retry: shouldRetry, 304 retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), 305 }); 306} 307