Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions app/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,21 @@
*
* Copyright Oxide Computer Company
*/
import { QueryClient } from '@tanstack/react-query'
import { QueryClient, useQuery, type UseQueryOptions } from '@tanstack/react-query'

import { Api } from './__generated__/Api'
import { type ApiError } from './errors'
import {
ensurePrefetched,
getApiQueryOptions,
getListQueryOptionsFn,
getUseApiMutation,
getUseApiQueries,
getUseApiQuery,
getUseApiQueryErrorsAllowed,
getUsePrefetchedApiQuery,
wrapQueryClient,
} from './hooks'

export {
ensurePrefetched,
usePrefetchedQuery,
PAGE_SIZE,
type PaginatedQuery,
} from './hooks'

export const api = new Api({
// unit tests run in Node, whose fetch implementation requires a full URL
host: process.env.NODE_ENV === 'test' ? 'http://testhost' : '',
Expand All @@ -42,7 +36,6 @@ export const apiq = getApiQueryOptions(api.methods)
*/
export const getListQFn = getListQueryOptionsFn(api.methods)
export const useApiQuery = getUseApiQuery(api.methods)
export const useApiQueries = getUseApiQueries(api.methods)
/**
* Same as `useApiQuery`, except we use `invariant(data)` to ensure the data is
* already there in the cache at request time, which means it has been
Expand All @@ -53,6 +46,9 @@ export const usePrefetchedApiQuery = getUsePrefetchedApiQuery(api.methods)
export const useApiQueryErrorsAllowed = getUseApiQueryErrorsAllowed(api.methods)
export const useApiMutation = getUseApiMutation(api.methods)

export const usePrefetchedQuery = <TData>(options: UseQueryOptions<TData, ApiError>) =>
ensurePrefetched(useQuery(options), options.queryKey)

// Needs to be defined here instead of in app so we can use it to define
// `apiQueryClient`, which provides API-typed versions of QueryClient methods
export const queryClient = new QueryClient({
Expand Down
70 changes: 15 additions & 55 deletions app/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ import {
hashKey,
queryOptions,
useMutation,
useQueries,
useQuery,
type DefaultError,
type FetchQueryOptions,
type InvalidateQueryFilters,
type QueryClient,
type QueryKey,
type UndefinedInitialDataOptions,
type UseMutationOptions,
type UseQueryOptions,
type UseQueryResult,
Expand All @@ -29,17 +26,12 @@ import { invariant } from '~/util/invariant'
import type { ApiResult } from './__generated__/Api'
import { processServerError, type ApiError } from './errors'
import { navToLogin } from './nav-to-login'
import { type ResultsPage } from './util'

/* eslint-disable @typescript-eslint/no-explicit-any */
export type Params<F> = F extends (p: infer P) => any ? P : never
export type Result<F> = F extends (p: any) => Promise<ApiResult<infer R>> ? R : never
export type ResultItem<F> =
Result<F> extends { items: (infer R)[] }
? R extends Record<string, unknown>
? R
: never
: never
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResultItem was only used by the old QueryTable, I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oof; that's an ugly one

type Params<F> = F extends (p: infer P) => any ? P : never
type Result<F> = F extends (p: any) => Promise<ApiResult<infer R>> ? R : never

export type ResultsPage<TItem> = { items: TItem[]; nextPage?: string }

type ApiClient = Record<string, (...args: any) => Promise<ApiResult<any>>>
/* eslint-enable @typescript-eslint/no-explicit-any */
Expand Down Expand Up @@ -92,17 +84,17 @@ Error message: ${error.message.replace(/\n/g, '\n' + ' '.repeat('Error message:
* `queryKey` and `queryFn` are always constructed by our helper hooks, so we
* only allow the rest of the options.
*/
type UseQueryOtherOptions<T, E = DefaultError> = Omit<
UndefinedInitialDataOptions<T, E>,
'queryKey' | 'queryFn'
type UseQueryOtherOptions<T> = Omit<
UseQueryOptions<T, ApiError>,
'queryKey' | 'queryFn' | 'initialData'
>

/**
* `queryKey` and `queryFn` are always constructed by our helper hooks, so we
* only allow the rest of the options.
*/
type FetchQueryOtherOptions<T, E = DefaultError> = Omit<
FetchQueryOptions<T, E>,
type FetchQueryOtherOptions<T> = Omit<
FetchQueryOptions<T, ApiError>,
'queryKey' | 'queryFn'
>

Expand All @@ -111,7 +103,7 @@ export const getApiQueryOptions =
<M extends string & keyof A>(
method: M,
params: Params<A[M]>,
options: UseQueryOtherOptions<Result<A[M]>, ApiError> = {}
options: UseQueryOtherOptions<Result<A[M]>> = {}
) =>
queryOptions({
queryKey: [method, params],
Expand Down Expand Up @@ -163,7 +155,7 @@ export const getListQueryOptionsFn =
>(
method: M,
params: Params<A[M]>,
options: UseQueryOtherOptions<Result<A[M]>, ApiError> = {}
options: UseQueryOtherOptions<Result<A[M]>> = {}
): PaginatedQuery<Result<A[M]>> => {
// We pull limit out of the query params rather than passing it in some
// other way so that there is exactly one way of specifying it. If we had
Expand All @@ -190,7 +182,7 @@ export const getUseApiQuery =
<M extends string & keyof A>(
method: M,
params: Params<A[M]>,
options: UseQueryOtherOptions<Result<A[M]>, ApiError> = {}
options: UseQueryOtherOptions<Result<A[M]>> = {}
) =>
useQuery(getApiQueryOptions(api)(method, params, options))

Expand All @@ -199,7 +191,7 @@ export const getUsePrefetchedApiQuery =
<M extends string & keyof A>(
method: M,
params: Params<A[M]>,
options: UseQueryOtherOptions<Result<A[M]>, ApiError> = {}
options: UseQueryOtherOptions<Result<A[M]>> = {}
) => {
const qOptions = getApiQueryOptions(api)(method, params, options)
return ensurePrefetched(useQuery(qOptions), qOptions.queryKey)
Expand Down Expand Up @@ -232,9 +224,6 @@ export function ensurePrefetched<TData, TError>(
return result as SetNonNullable<typeof result, 'data'>
}

export const usePrefetchedQuery = <TData>(options: UseQueryOptions<TData, ApiError>) =>
ensurePrefetched(useQuery(options), options.queryKey)

const ERRORS_ALLOWED = 'errors-allowed'

/** Result that includes both success and error so it can be cached by RQ */
Expand Down Expand Up @@ -289,35 +278,6 @@ export const getUseApiMutation =
...options,
})

/**
* Our version of `useQueries`, but with the key difference that all queries in
* a given call are using the same API method, and therefore all have the same
* request and response (`Params` and `Result`) types. Otherwise the types would
* be (perhaps literally) impossible.
*/
export const getUseApiQueries =
<A extends ApiClient>(api: A) =>
<M extends string & keyof A>(
method: M,
paramsArray: Params<A[M]>[],
options: UseQueryOtherOptions<Result<A[M]>, ApiError> = {}
) => {
return useQueries({
queries: paramsArray.map(
(params) =>
({
queryKey: [method, params],
queryFn: ({ signal }) =>
api[method](params, { signal }).then(handleResult(method)),
throwOnError: (err: ApiError) => err.statusCode === 404,
...options,
// Add params to the result for reassembly after the queries are returned
select: (data) => ({ ...data, params }),
}) satisfies UseQueryOptions<Result<A[M]> & { params: Params<A[M]> }, ApiError>
),
})
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (and useApiQueries) wasn't used. I kept it around in case we needed it again, but I think if we wanted this now, we'd use the query options helper with map and pass the resulting array of options objects to the built-in useQueries.

export const wrapQueryClient = <A extends ApiClient>(api: A, queryClient: QueryClient) => ({
/**
* Note that we only take a single argument, `method`, rather than allowing
Expand All @@ -340,7 +300,7 @@ export const wrapQueryClient = <A extends ApiClient>(api: A, queryClient: QueryC
fetchQuery: <M extends string & keyof A>(
method: M,
params: Params<A[M]>,
options: FetchQueryOtherOptions<Result<A[M]>, ApiError> = {}
options: FetchQueryOtherOptions<Result<A[M]>> = {}
) =>
queryClient.fetchQuery({
queryKey: [method, params],
Expand All @@ -350,7 +310,7 @@ export const wrapQueryClient = <A extends ApiClient>(api: A, queryClient: QueryC
prefetchQuery: <M extends string & keyof A>(
method: M,
params: Params<A[M]>,
options: FetchQueryOtherOptions<Result<A[M]>, ApiError> = {}
options: FetchQueryOtherOptions<Result<A[M]>> = {}
) =>
queryClient.prefetchQuery({
queryKey: [method, params],
Expand Down
2 changes: 1 addition & 1 deletion app/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ export type { ApiTypes }

export * as PathParams from './path-params'

export type { Params, Result, ResultItem } from './hooks'
export { ensurePrefetched, PAGE_SIZE, type PaginatedQuery, type ResultsPage } from './hooks'
export type { ApiError } from './errors'
export { navToLogin } from './nav-to-login'
2 changes: 0 additions & 2 deletions app/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import type {
VpcFirewallRuleUpdate,
} from './__generated__/Api'

export type ResultsPage<TItem> = { items: TItem[]; nextPage?: string }

// API limits encoded in https://github.com/oxidecomputer/omicron/blob/main/nexus/src/app/mod.rs

export const MAX_NICS_PER_INSTANCE = 8
Expand Down
Loading