Skip to content

Commit 76ca8cf

Browse files
authored
Add errors-allowed version of apiq and use it everywhere (#2879)
add errors-allowed version of apiq and use it everywhere
1 parent 777a959 commit 76ca8cf

File tree

5 files changed

+35
-26
lines changed

5 files changed

+35
-26
lines changed

app/api/client.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import { type ApiError } from './errors'
1616
import {
1717
ensurePrefetched,
1818
getApiQueryOptions,
19+
getApiQueryOptionsErrorsAllowed,
1920
getListQueryOptionsFn,
2021
getUseApiMutation,
2122
getUseApiQuery,
22-
getUseApiQueryErrorsAllowed,
2323
getUsePrefetchedApiQuery,
2424
wrapQueryClient,
2525
} from './hooks'
@@ -33,6 +33,23 @@ export type ApiMethods = typeof api.methods
3333

3434
/** API-specific query options helper. */
3535
export const apiq = getApiQueryOptions(api.methods)
36+
/**
37+
* Variant of `apiq` that allows error responses as a valid result,
38+
* which importantly means they can be cached by RQ. This means we can prefetch
39+
* an endpoint that might error (see `prefetchQueryErrorsAllowed`) and use this
40+
* hook to retrieve the error result.
41+
*
42+
* Concretely, the difference from the usual query function is that we turn all
43+
* errors into successes. Instead of throwing the error, we return it as a valid
44+
* result. This means `data` has a type that includes the possibility of error,
45+
* plus a discriminant to let us handle both sides properly in the calling code.
46+
*
47+
* We also use a special query key to distinguish these from normal API queries.
48+
* If we hit a given endpoint twice on the same page, once the normal way and
49+
* once with errors allowed, the responses have different shapes, so we do not
50+
* want to share the cache and mix them up.
51+
*/
52+
export const apiqErrorsAllowed = getApiQueryOptionsErrorsAllowed(api.methods)
3653
/**
3754
* Query options helper that only supports list endpoints. Returns
3855
* a function `(limit, pageToken) => QueryOptions` for use with
@@ -47,7 +64,6 @@ export const useApiQuery = getUseApiQuery(api.methods)
4764
* test loading the page to exercise the invariant in CI.
4865
*/
4966
export const usePrefetchedApiQuery = getUsePrefetchedApiQuery(api.methods)
50-
export const useApiQueryErrorsAllowed = getUseApiQueryErrorsAllowed(api.methods)
5167
export const useApiMutation = getUseApiMutation(api.methods)
5268

5369
export const usePrefetchedQuery = <TData>(options: UseQueryOptions<TData, ApiError>) =>

app/api/hooks.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -229,27 +229,16 @@ const ERRORS_ALLOWED = 'errors-allowed'
229229
/** Result that includes both success and error so it can be cached by RQ */
230230
type ErrorsAllowed<T, E> = { type: 'success'; data: T } | { type: 'error'; data: E }
231231

232-
/**
233-
* Variant of `getUseApiQuery` that allows error responses as a valid result,
234-
* which importantly means they can be cached by RQ. This means we can prefetch
235-
* an endpoint that might error (see `prefetchQueryErrorsAllowed`) and use this
236-
* hook to retrieve the error result.
237-
*
238-
* Concretely, the only difference from `getUseApiQuery`: we turn all errors
239-
* into successes. Instead of throwing the error, we return it as a valid
240-
* result. This means `data` has a type that includes the possibility of error,
241-
* plus a discriminant to let us handle both sides properly in the calling code.
242-
*/
243-
export const getUseApiQueryErrorsAllowed =
232+
export const getApiQueryOptionsErrorsAllowed =
244233
<A extends ApiClient>(api: A) =>
245234
<M extends string & keyof A>(
246235
method: M,
247236
params: Params<A[M]>,
248237
options: UseQueryOtherOptions<ErrorsAllowed<Result<A[M]>, ApiError>> = {}
249-
) => {
250-
return useQuery({
238+
) =>
239+
queryOptions({
251240
// extra bit of key is important to distinguish from normal query. if we
252-
// hit a a given endpoint twice on the same page, once the normal way and
241+
// hit a given endpoint twice on the same page, once the normal way and
253242
// once with errors allowed the responses have different shapes, so we do
254243
// not want to share the cache and mix them up
255244
queryKey: [method, params, ERRORS_ALLOWED],
@@ -260,7 +249,6 @@ export const getUseApiQueryErrorsAllowed =
260249
.catch((data) => ({ type: 'error' as const, data })),
261250
...options,
262251
})
263-
}
264252

265253
export const getUseApiMutation =
266254
<A extends ApiClient>(api: A) =>

app/hooks/use-current-user.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
* Copyright Oxide Computer Company
77
*/
88

9-
import { useApiQueryErrorsAllowed, usePrefetchedApiQuery } from '~/api/client'
9+
import { useQuery } from '@tanstack/react-query'
10+
11+
import { apiqErrorsAllowed, usePrefetchedApiQuery } from '~/api/client'
1012
import { invariant } from '~/util/invariant'
1113

1214
/**
@@ -24,7 +26,7 @@ export function useCurrentUser() {
2426
// the fleet (system) policy, but if the user doesn't have fleet read, we'll
2527
// get a 403 from that endpoint. So we simply check whether that endpoint 200s
2628
// or not to determine whether the user is a fleet viewer.
27-
const { data: systemPolicy } = useApiQueryErrorsAllowed('systemPolicyView', {})
29+
const { data: systemPolicy } = useQuery(apiqErrorsAllowed('systemPolicyView', {}))
2830
// don't use usePrefetchedApiQuery because it's not worth making an errors
2931
// allowed version of that
3032
invariant(systemPolicy, 'System policy must be prefetched')

app/pages/project/snapshots/SnapshotsPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import { useQuery } from '@tanstack/react-query'
89
import { createColumnHelper } from '@tanstack/react-table'
910
import { useCallback } from 'react'
1011
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
1112

1213
import {
14+
apiqErrorsAllowed,
1315
apiQueryClient,
1416
getListQFn,
1517
queryClient,
1618
useApiMutation,
1719
useApiQueryClient,
18-
useApiQueryErrorsAllowed,
1920
type Snapshot,
2021
} from '@oxide/api'
2122
import { Snapshots16Icon, Snapshots24Icon } from '@oxide/design-system/icons/react'
@@ -38,7 +39,7 @@ import { docLinks } from '~/util/links'
3839
import { pb } from '~/util/path-builder'
3940

4041
const DiskNameFromId = ({ value }: { value: string }) => {
41-
const { data } = useApiQueryErrorsAllowed('diskView', { path: { disk: value } })
42+
const { data } = useQuery(apiqErrorsAllowed('diskView', { path: { disk: value } }))
4243

4344
if (!data) return <SkeletonCell />
4445
if (data.type === 'error') return <Badge color="neutral">Deleted</Badge>

app/table/cells/IpPoolCell.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { useApiQueryErrorsAllowed } from '~/api'
8+
import { useQuery } from '@tanstack/react-query'
9+
10+
import { apiqErrorsAllowed } from '~/api'
911
import { Tooltip } from '~/ui/lib/Tooltip'
1012

1113
import { EmptyCell, SkeletonCell } from './EmptyCell'
1214

1315
export const IpPoolCell = ({ ipPoolId }: { ipPoolId: string }) => {
14-
const { data: result } = useApiQueryErrorsAllowed('projectIpPoolView', {
15-
path: { pool: ipPoolId },
16-
})
16+
const { data: result } = useQuery(
17+
apiqErrorsAllowed('projectIpPoolView', { path: { pool: ipPoolId } })
18+
)
1719
if (!result) return <SkeletonCell />
1820
// this should essentially never happen, but it's probably better than blowing
1921
// up the whole page if the pool is not found

0 commit comments

Comments
 (0)