Skip to content

Commit

Permalink
feat: add prefixQueryKey to createClient
Browse files Browse the repository at this point in the history
  • Loading branch information
jeiea committed Nov 14, 2024
1 parent e864bec commit b92ca29
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 33 deletions.
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.
66 changes: 33 additions & 33 deletions packages/openapi-react-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
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 +39,23 @@ 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 +66,13 @@ 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 +96,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, T = unknown>(
client: FetchClient<Paths, Media>,
): OpenapiQueryClient<Paths, Media> {
{ prefixQueryKey }: { prefixQueryKey?: T } = {},
): OpenapiQueryClient<Paths, Media, T> {
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<T, 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 +122,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, T> = (method, path, ...[init, options]) => ({
queryKey: [prefixQueryKey as T, 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

0 comments on commit b92ca29

Please sign in to comment.