@@ -21,13 +21,15 @@ import {
2121 type UseQueryOptions ,
2222 type UseQueryResult ,
2323} from '@tanstack/react-query'
24+ import * as R from 'remeda'
2425import { type SetNonNullable } from 'type-fest'
2526
2627import { invariant } from '~/util/invariant'
2728
2829import type { ApiResult } from './__generated__/Api'
2930import { processServerError , type ApiError } from './errors'
3031import { navToLogin } from './nav-to-login'
32+ import { type ResultsPage } from './util'
3133
3234/* eslint-disable @typescript-eslint/no-explicit-any */
3335export type Params < F > = F extends ( p : infer P ) => any ? P : never
@@ -123,6 +125,66 @@ export const getApiQueryOptions =
123125 ...options ,
124126 } )
125127
128+ // Managed here instead of at the display layer so it can be built into the
129+ // query options and shared between loader prefetch and QueryTable
130+ export const PAGE_SIZE = 25
131+
132+ /**
133+ * This primarily exists so we can have an object that encapsulates everything
134+ * useQueryTable needs to know about a query. In particular, it needs the page
135+ * size, and you can't pull that out of the query options object unless you
136+ * stick it in `meta`, and then we don't have type safety.
137+ */
138+ export type PaginatedQuery < TData > = {
139+ optionsFn : (
140+ pageToken ?: string
141+ ) => UseQueryOptions < TData , ApiError > & { queryKey : QueryKey }
142+ pageSize : number
143+ }
144+
145+ /**
146+ * This is the same as getApiQueryOptions except for two things:
147+ *
148+ * 1. We use a type constraint on the method key to ensure it can
149+ * only be used with endpoints that return a `ResultsPage`.
150+ * 2. Instead of returning the options directly, it returns a paginated
151+ * query config object containing the page size and a function that
152+ * takes `limit` and `pageToken` and merges them into the query params
153+ * so that these can be passed in by `QueryTable`.
154+ */
155+ export const getListQueryOptionsFn =
156+ < A extends ApiClient > ( api : A ) =>
157+ <
158+ M extends string &
159+ {
160+ // this helper can only be used with endpoints that return ResultsPage
161+ [ K in keyof A ] : Result < A [ K ] > extends ResultsPage < unknown > ? K : never
162+ } [ keyof A ] ,
163+ > (
164+ method : M ,
165+ params : Params < A [ M ] > ,
166+ options : UseQueryOtherOptions < Result < A [ M ] > , ApiError > = { }
167+ ) : PaginatedQuery < Result < A [ M ] > > => {
168+ // We pull limit out of the query params rather than passing it in some
169+ // other way so that there is exactly one way of specifying it. If we had
170+ // some other way of doing it, and then you also passed it in as a query
171+ // param, it would be hard to guess which takes precedence. (pathOr plays
172+ // nice when the properties don't exist.)
173+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174+ const limit = R . pathOr ( params as any , [ 'query' , 'limit' ] , PAGE_SIZE )
175+ return {
176+ optionsFn : ( pageToken ?: string ) => {
177+ const newParams = { ...params , query : { ...params . query , limit, pageToken } }
178+ return getApiQueryOptions ( api ) ( method , newParams , {
179+ ...options ,
180+ // identity function so current page sticks around while next loads
181+ placeholderData : ( x ) => x ,
182+ } )
183+ } ,
184+ pageSize : limit ,
185+ }
186+ }
187+
126188export const getUseApiQuery =
127189 < A extends ApiClient > ( api : A ) =>
128190 < M extends string & keyof A > (
@@ -140,7 +202,7 @@ export const getUsePrefetchedApiQuery =
140202 options : UseQueryOtherOptions < Result < A [ M ] > , ApiError > = { }
141203 ) => {
142204 const qOptions = getApiQueryOptions ( api ) ( method , params , options )
143- return ensure ( useQuery ( qOptions ) , qOptions . queryKey )
205+ return ensurePrefetched ( useQuery ( qOptions ) , qOptions . queryKey )
144206 }
145207
146208const prefetchError = ( key ?: QueryKey ) =>
@@ -152,7 +214,11 @@ Ensure the following:
152214• request isn't erroring-out server-side (check the Networking tab)
153215• mock API endpoint is implemented in handlers.ts`
154216
155- export function ensure < TData , TError > (
217+ /**
218+ * Ensure a query result came from the cache by blowing up if `data` comes
219+ * back undefined.
220+ */
221+ export function ensurePrefetched < TData , TError > (
156222 result : UseQueryResult < TData , TError > ,
157223 /**
158224 * Optional because if we call this manually from a component like
0 commit comments