Skip to content

Commit 9b31801

Browse files
committed
feat: ✨ infiniteQueryOptions add with test code
1 parent c53536c commit 9b31801

File tree

2 files changed

+187
-39
lines changed

2 files changed

+187
-39
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>,
@@ -167,6 +220,7 @@ export type UseMutationMethod<Paths extends Record<string, Record<HttpMethod, {}
167220

168221
export interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
169222
queryOptions: QueryOptionsFunction<Paths, Media>;
223+
infiniteQueryOptions: InfiniteQueryOptionsFunction<Paths, Media>;
170224
useQuery: UseQueryMethod<Paths, Media>;
171225
useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
172226
useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;
@@ -215,44 +269,47 @@ export default function createClient<Paths extends {}, Media extends MediaType =
215269
...options,
216270
});
217271

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

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

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
2-
import { server, baseUrl, useMockRequestHandler } from "./fixtures/mock-server.js";
3-
import type { paths } from "./fixtures/api.js";
4-
import createClient, { type MethodResponse } from "../src/index.js";
1+
import { afterAll, afterEach, beforeAll, describe, expect, it, expectTypeOf, vi } from "vitest";
2+
import { server, baseUrl, useMockRequestHandler } from "./fixtures/mock-server";
3+
import type { paths } from "./fixtures/api";
4+
import createClient, { type MethodResponse } from "../src/api/index";
55
import createFetchClient from "openapi-fetch";
66
import { fireEvent, render, renderHook, screen, waitFor, act } from "@testing-library/react";
77
import {
@@ -11,6 +11,7 @@ import {
1111
useQuery,
1212
useSuspenseQuery,
1313
skipToken,
14+
useInfiniteQuery,
1415
} from "@tanstack/react-query";
1516
import { Suspense, type ReactNode } from "react";
1617
import { ErrorBoundary } from "react-error-boundary";
@@ -75,9 +76,91 @@ describe("client", () => {
7576
expect(client).toHaveProperty("useQuery");
7677
expect(client).toHaveProperty("useSuspenseQuery");
7778
expect(client).toHaveProperty("useMutation");
79+
if ("infiniteQueryOptions" in client) {
80+
expect(client).toHaveProperty("infiniteQueryOptions");
81+
}
7882
});
7983

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

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

163249
expectTypeOf(result.current[1]).toEqualTypeOf<(typeof result.current)[0]>();
164250

@@ -170,7 +256,10 @@ describe("client", () => {
170256
}
171257
| undefined
172258
>();
173-
expectTypeOf(result.current[2].error).toEqualTypeOf<{ code: number; message: string } | null>();
259+
expectTypeOf(result.current[2].error).toEqualTypeOf<{
260+
code: number;
261+
message: string;
262+
} | null>();
174263

175264
expectTypeOf(result.current[3]).toEqualTypeOf<(typeof result.current)[2]>();
176265

@@ -811,7 +900,9 @@ describe("client", () => {
811900
wrapper,
812901
});
813902

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

816907
expect(data.message).toBe("Hello");
817908
});

0 commit comments

Comments
 (0)