Skip to content

Commit 4c2e7a2

Browse files
committed
feat: ✨ infiniteQueryOptions add with test code
1 parent 6ed0634 commit 4c2e7a2

File tree

2 files changed

+182
-35
lines changed

2 files changed

+182
-35
lines changed

packages/openapi-react-query/src/index.ts

Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,59 @@ export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod,
7979
}
8080
>;
8181

82+
// Helper type to infer TPageParam type
83+
type InferPageParamType<T> = T extends { initialPageParam: infer P } ? P : unknown;
84+
85+
export type InfiniteQueryOptionsFunction<
86+
Paths extends Record<string, Record<HttpMethod, {}>>,
87+
Media extends MediaType,
88+
> = <
89+
Method extends HttpMethod,
90+
Path extends PathsWithMethod<Paths, Method>,
91+
Init extends MaybeOptionalInit<Paths[Path], Method>,
92+
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>,
93+
Options extends Omit<
94+
UseInfiniteQueryOptions<
95+
Response["data"],
96+
Response["error"],
97+
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
98+
QueryKey<Paths, Method, Path>,
99+
InferPageParamType<Options>
100+
>,
101+
"queryKey" | "queryFn"
102+
> & {
103+
pageParamName?: string;
104+
initialPageParam: InferPageParamType<Options>;
105+
},
106+
>(
107+
method: Method,
108+
path: Path,
109+
init: InitWithUnknowns<Init>,
110+
options: Options,
111+
) => NoInfer<
112+
Omit<
113+
UseInfiniteQueryOptions<
114+
Response["data"],
115+
Response["error"],
116+
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
117+
QueryKey<Paths, Method, Path>,
118+
InferPageParamType<Options>
119+
>,
120+
"queryFn"
121+
> & {
122+
queryFn: Exclude<
123+
UseInfiniteQueryOptions<
124+
Response["data"],
125+
Response["error"],
126+
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
127+
QueryKey<Paths, Method, Path>,
128+
InferPageParamType<Options>
129+
>["queryFn"],
130+
SkipToken | undefined
131+
>;
132+
}
133+
>;
134+
82135
export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
83136
Method extends HttpMethod,
84137
Path extends PathsWithMethod<Paths, Method>,
@@ -166,6 +219,7 @@ export type UseMutationMethod<Paths extends Record<string, Record<HttpMethod, {}
166219

167220
export interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
168221
queryOptions: QueryOptionsFunction<Paths, Media>;
222+
infiniteQueryOptions: InfiniteQueryOptionsFunction<Paths, Media>;
169223
useQuery: UseQueryMethod<Paths, Media>;
170224
useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
171225
useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;
@@ -214,44 +268,47 @@ export default function createClient<Paths extends {}, Media extends MediaType =
214268
...options,
215269
});
216270

271+
const infiniteQueryOptions: InfiniteQueryOptionsFunction<Paths, Media> = (method, path, init, options) => {
272+
const { pageParamName = "cursor", initialPageParam, ...restOptions } = options;
273+
const { queryKey } = queryOptions(method, path, init);
274+
275+
return {
276+
queryKey,
277+
initialPageParam,
278+
queryFn: async ({ queryKey: [method, path, init], pageParam, signal }) => {
279+
const mth = method.toUpperCase() as Uppercase<typeof method>;
280+
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
281+
const mergedInit = {
282+
...init,
283+
signal,
284+
params: {
285+
...(init?.params || {}),
286+
query: {
287+
...(init?.params as { query?: DefaultParamsOption })?.query,
288+
[pageParamName]: pageParam,
289+
},
290+
},
291+
};
292+
293+
const { data, error } = await fn(path, mergedInit as any);
294+
if (error) {
295+
throw error;
296+
}
297+
return data;
298+
},
299+
...restOptions,
300+
};
301+
};
302+
217303
return {
218304
queryOptions,
305+
infiniteQueryOptions,
219306
useQuery: (method, path, ...[init, options, queryClient]) =>
220307
useQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),
221308
useSuspenseQuery: (method, path, ...[init, options, queryClient]) =>
222309
useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),
223-
useInfiniteQuery: (method, path, init, options, queryClient) => {
224-
const { pageParamName = "cursor", ...restOptions } = options;
225-
const { queryKey } = queryOptions(method, path, init);
226-
return useInfiniteQuery(
227-
{
228-
queryKey,
229-
queryFn: async ({ queryKey: [method, path, init], pageParam = 0, signal }) => {
230-
const mth = method.toUpperCase() as Uppercase<typeof method>;
231-
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
232-
const mergedInit = {
233-
...init,
234-
signal,
235-
params: {
236-
...(init?.params || {}),
237-
query: {
238-
...(init?.params as { query?: DefaultParamsOption })?.query,
239-
[pageParamName]: pageParam,
240-
},
241-
},
242-
};
243-
244-
const { data, error } = await fn(path, mergedInit as any);
245-
if (error) {
246-
throw error;
247-
}
248-
return data;
249-
},
250-
...restOptions,
251-
},
252-
queryClient,
253-
);
254-
},
310+
useInfiniteQuery: (method, path, init, options, queryClient) =>
311+
useInfiniteQuery(infiniteQueryOptions(method, path, init, options), queryClient),
255312
useMutation: (method, path, options, queryClient) =>
256313
useMutation(
257314
{

packages/openapi-react-query/test/index.test.tsx

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,91 @@ describe("client", () => {
7575
expect(client).toHaveProperty("useQuery");
7676
expect(client).toHaveProperty("useSuspenseQuery");
7777
expect(client).toHaveProperty("useMutation");
78+
if ("infiniteQueryOptions" in client) {
79+
expect(client).toHaveProperty("infiniteQueryOptions");
80+
}
7881
});
7982

8083
describe("queryOptions", () => {
84+
describe("infiniteQueryOptions", () => {
85+
it("returns infinite query options that can be passed to useInfiniteQuery", async () => {
86+
const fetchClient = createFetchClient<paths>({ baseUrl });
87+
const client = createClient(fetchClient);
88+
89+
if (!("infiniteQueryOptions" in client)) return;
90+
91+
const options = client.infiniteQueryOptions(
92+
"get",
93+
"/paginated-data",
94+
{
95+
params: {
96+
query: {
97+
limit: 3,
98+
},
99+
},
100+
},
101+
{
102+
getNextPageParam: (lastPage) => lastPage.nextPage,
103+
initialPageParam: 0,
104+
},
105+
);
106+
107+
expect(options).toHaveProperty("queryKey");
108+
expect(options).toHaveProperty("queryFn");
109+
expect(Array.isArray(options.queryKey)).toBe(true);
110+
expectTypeOf(options.queryFn).toBeFunction();
111+
112+
useMockRequestHandler({
113+
baseUrl,
114+
method: "get",
115+
path: "/paginated-data",
116+
status: 200,
117+
body: { items: [1, 2, 3], nextPage: 1 },
118+
});
119+
120+
const { result } = renderHook(() => useInfiniteQuery(options), { wrapper });
121+
122+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
123+
expect(result.current.data?.pages[0].items).toEqual([1, 2, 3]);
124+
});
125+
126+
it("returns infinite query options with custom pageParamName", async () => {
127+
const fetchClient = createFetchClient<paths>({ baseUrl });
128+
const client = createClient(fetchClient);
129+
130+
if (!("infiniteQueryOptions" in client)) return;
131+
132+
const options = client.infiniteQueryOptions(
133+
"get",
134+
"/paginated-data",
135+
{
136+
params: {
137+
query: {
138+
limit: 3,
139+
},
140+
},
141+
},
142+
{
143+
getNextPageParam: (lastPage) => lastPage.nextPage,
144+
initialPageParam: 0,
145+
pageParamName: "follow_cursor",
146+
},
147+
);
148+
149+
useMockRequestHandler({
150+
baseUrl,
151+
method: "get",
152+
path: "/paginated-data",
153+
status: 200,
154+
body: { items: [1, 2, 3], nextPage: 1 },
155+
});
156+
157+
const { result } = renderHook(() => useInfiniteQuery(options), { wrapper });
158+
159+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
160+
expect(result.current.data?.pages[0].items).toEqual([1, 2, 3]);
161+
});
162+
});
81163
it("has correct parameter types", async () => {
82164
const fetchClient = createFetchClient<paths>({ baseUrl });
83165
const client = createClient(fetchClient);
@@ -158,7 +240,10 @@ describe("client", () => {
158240
);
159241

160242
expectTypeOf(result.current[0].data).toEqualTypeOf<string[] | undefined>();
161-
expectTypeOf(result.current[0].error).toEqualTypeOf<{ code: number; message: string } | null>();
243+
expectTypeOf(result.current[0].error).toEqualTypeOf<{
244+
code: number;
245+
message: string;
246+
} | null>();
162247

163248
expectTypeOf(result.current[1]).toEqualTypeOf<(typeof result.current)[0]>();
164249

@@ -170,7 +255,10 @@ describe("client", () => {
170255
}
171256
| undefined
172257
>();
173-
expectTypeOf(result.current[2].error).toEqualTypeOf<{ code: number; message: string } | null>();
258+
expectTypeOf(result.current[2].error).toEqualTypeOf<{
259+
code: number;
260+
message: string;
261+
} | null>();
174262

175263
expectTypeOf(result.current[3]).toEqualTypeOf<(typeof result.current)[2]>();
176264

@@ -811,7 +899,9 @@ describe("client", () => {
811899
wrapper,
812900
});
813901

814-
const data = await result.current.mutateAsync({ body: { message: "Hello", replied_at: 0 } });
902+
const data = await result.current.mutateAsync({
903+
body: { message: "Hello", replied_at: 0 },
904+
});
815905

816906
expect(data.message).toBe("Hello");
817907
});

0 commit comments

Comments
 (0)