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