Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/good-buttons-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openapi-react-query': minor
---

Add `prefixQueryKey` to `createClient` to avoid query key collision between different openapi-fetch clients.
88 changes: 55 additions & 33 deletions packages/openapi-react-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
import {
type UseMutationOptions,
type UseMutationResult,
type UseQueryOptions,
type UseQueryResult,
type UseSuspenseQueryOptions,
type UseSuspenseQueryResult,
type QueryClient,
type QueryFunctionContext,
type SkipToken,
useMutation,
useQuery,
useSuspenseQuery,
import type {
QueryClient,
QueryFunctionContext,
SkipToken,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
UseSuspenseQueryOptions,
UseSuspenseQueryResult,
} from "@tanstack/react-query";
import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient } from "openapi-fetch";
import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query";
import type { ClientMethod, Client as FetchClient, FetchResponse, MaybeOptionalInit } from "openapi-fetch";
import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers";

type InitWithUnknowns<Init> = Init & { [key: string]: unknown };

export type QueryKey<
Prefix,
Paths extends Record<string, Record<HttpMethod, {}>>,
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
> = readonly [Method, Path, MaybeOptionalInit<Paths[Path], Method>];
> = readonly [Prefix, Method, Path, MaybeOptionalInit<Paths[Path], Method>];

export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
export type QueryOptionsFunction<
Paths extends Record<string, Record<HttpMethod, {}>>,
Media extends MediaType,
Prefix = unknown,
> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
Options extends Omit<
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Prefix, Paths, Method, Path>>,
"queryKey" | "queryFn"
>,
>(
Expand All @@ -40,23 +43,32 @@ export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod,
: [InitWithUnknowns<Init>, Options?]
) => NoInfer<
Omit<
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Prefix, Paths, Method, Path>>,
"queryFn"
> & {
queryFn: Exclude<
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>["queryFn"],
UseQueryOptions<
Response["data"],
Response["error"],
Response["data"],
QueryKey<Prefix, Paths, Method, Path>
>["queryFn"],
SkipToken | undefined
>;
}
>;

export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
export type UseQueryMethod<
Paths extends Record<string, Record<HttpMethod, {}>>,
Media extends MediaType,
Prefix = unknown,
> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
Options extends Omit<
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
UseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Prefix, Paths, Method, Path>>,
"queryKey" | "queryFn"
>,
>(
Expand All @@ -67,13 +79,22 @@ export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>,
: [InitWithUnknowns<Init>, Options?, QueryClient?]
) => UseQueryResult<Response["data"], Response["error"]>;

export type UseSuspenseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
export type UseSuspenseQueryMethod<
Paths extends Record<string, Record<HttpMethod, {}>>,
Media extends MediaType,
Prefix = unknown,
> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
Options extends Omit<
UseSuspenseQueryOptions<Response["data"], Response["error"], Response["data"], QueryKey<Paths, Method, Path>>,
UseSuspenseQueryOptions<
Response["data"],
Response["error"],
Response["data"],
QueryKey<Prefix, Paths, Method, Path>
>,
"queryKey" | "queryFn"
>,
>(
Expand All @@ -97,21 +118,22 @@ export type UseMutationMethod<Paths extends Record<string, Record<HttpMethod, {}
queryClient?: QueryClient,
) => UseMutationResult<Response["data"], Response["error"], Init>;

export interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
queryOptions: QueryOptionsFunction<Paths, Media>;
useQuery: UseQueryMethod<Paths, Media>;
useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
export interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType, Prefix = unknown> {
queryOptions: QueryOptionsFunction<Paths, Media, Prefix>;
useQuery: UseQueryMethod<Paths, Media, Prefix>;
useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media, Prefix>;
useMutation: UseMutationMethod<Paths, Media>;
}

// TODO: Add the ability to bring queryClient as argument
export default function createClient<Paths extends {}, Media extends MediaType = MediaType>(
export default function createClient<Paths extends {}, Media extends MediaType = MediaType, Prefix = unknown>(
client: FetchClient<Paths, Media>,
): OpenapiQueryClient<Paths, Media> {
{ prefixQueryKey }: { prefixQueryKey?: Prefix } = {},
): OpenapiQueryClient<Paths, Media, Prefix> {
const queryFn = async <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>>({
queryKey: [method, path, init],
queryKey: [, method, path, init],
signal,
}: QueryFunctionContext<QueryKey<Paths, Method, Path>>) => {
}: QueryFunctionContext<QueryKey<Prefix, Paths, Method, Path>>) => {
const mth = method.toUpperCase() as Uppercase<typeof method>;
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
const { data, error } = await fn(path, { signal, ...(init as any) }); // TODO: find a way to avoid as any
Expand All @@ -122,8 +144,8 @@ export default function createClient<Paths extends {}, Media extends MediaType =
return data;
};

const queryOptions: QueryOptionsFunction<Paths, Media> = (method, path, ...[init, options]) => ({
queryKey: [method, path, init as InitWithUnknowns<typeof init>] as const,
const queryOptions: QueryOptionsFunction<Paths, Media, Prefix> = (method, path, ...[init, options]) => ({
queryKey: [prefixQueryKey as Prefix, method, path, init as InitWithUnknowns<typeof init>] as const,
queryFn,
...options,
});
Expand Down
27 changes: 27 additions & 0 deletions packages/openapi-react-query/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,33 @@ describe("client", () => {
expectTypeOf(result.current.data).toEqualTypeOf<"select(true)">();
expectTypeOf(result.current.error).toEqualTypeOf<false | null>();
});

it("should differentiate queries by prefixQueryKey", async () => {
const fetchClient1 = createFetchClient<minimalGetPaths>({ baseUrl, fetch: fetchInfinite });
const fetchClient2 = createFetchClient<minimalGetPaths>({ baseUrl, fetch: fetchInfinite });
const client1 = createClient(fetchClient1);
const client11 = createClient(fetchClient1);
const client2 = createClient(fetchClient2, { prefixQueryKey: ["cache2"] as const });

renderHook(
() => {
useQueries({
queries: [
client1.queryOptions("get", "/foo"),
client11.queryOptions("get", "/foo"),
client2.queryOptions("get", "/foo"),
],
});
},
{ wrapper },
);

expectTypeOf(client1.queryOptions("get", "/foo").queryKey[0]).toEqualTypeOf<unknown>();
expectTypeOf(client2.queryOptions("get", "/foo").queryKey[0]).toEqualTypeOf<readonly ["cache2"]>();

// client1 and client11 have the same query key, so 3 - 1 = 2
expect(queryClient.isFetching()).toBe(2);
});
});

describe("useQuery", () => {
Expand Down