openapi-typesafe-react-query 0.0.0 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Martin Paucot
4
+ Copyright (c) 2026 Fedor Chishchin and openapi-typesafe contributors
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,67 @@
1
1
  # openapi-typesafe-react-query
2
2
 
3
- This name is reserved for openapi-typesafe. This version contains no code.
3
+ openapi-typesafe-react-query is a small, type-safe wrapper (about 2.5 kB) around [@tanstack/react-query](https://tanstack.com/query/latest/docs/framework/react/overview) to work with OpenAPI schema.
4
+
5
+ It works by using [openapi-typesafe-fetch](../openapi-typesafe-fetch) and [openapi-typesafe](../openapi-typesafe) so URLs, parameters, request bodies and responses are checked against your schema, with no manual types,
6
+ no `any` and no `as` assertions.
7
+
8
+ A maintained, drop-in successor to `openapi-react-query` (see the [repository README](../../README.md)).
9
+
10
+ > **Status: in development; not published to npm yet.**
11
+
12
+ ## Setup
13
+
14
+ Install this library along with [openapi-typesafe-fetch](../openapi-typesafe-fetch) and [openapi-typesafe](../openapi-typesafe):
15
+
16
+ ```bash
17
+ npm i openapi-typesafe-react-query openapi-typesafe-fetch
18
+ npm i -D openapi-typesafe typescript
19
+ ```
20
+
21
+ Next, generate TypeScript types from your OpenAPI schema using openapi-typesafe:
22
+
23
+ ```bash
24
+ npx openapi-typesafe ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ Once your types have been generated from your schema, you can create a [fetch client](../openapi-typesafe-fetch), a react-query client and start querying your API.
30
+
31
+ ```tsx
32
+ import createFetchClient from "openapi-typesafe-fetch";
33
+ import createClient from "openapi-typesafe-react-query";
34
+ import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typesafe
35
+
36
+ const fetchClient = createFetchClient<paths>({
37
+ baseUrl: "https://myapi.dev/v1/",
38
+ });
39
+ const $api = createClient(fetchClient);
40
+
41
+ const MyComponent = () => {
42
+ const { data, error, isPending } = $api.useQuery("get", "/blogposts/{post_id}", {
43
+ params: {
44
+ path: { post_id: "5" },
45
+ },
46
+ });
47
+
48
+ if (isPending || !data) return "Loading...";
49
+
50
+ if (error) return `An error occurred: ${error.message}`;
51
+
52
+ return <div>{data.title}</div>;
53
+ };
54
+ ```
55
+
56
+ > You can find more information about `createFetchClient` in the [openapi-typesafe-fetch documentation](../openapi-typesafe-fetch).
57
+
58
+ ## Migrating from openapi-react-query
59
+
60
+ ```diff
61
+ - import createClient from "openapi-react-query";
62
+ + import createClient from "openapi-typesafe-react-query";
63
+ ```
64
+
65
+ ## License
66
+
67
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const reactQuery = require('@tanstack/react-query');
4
+
5
+ const INFINITE_QUERY_KEY_MARKER = "infinite";
6
+ function throwIfFailed(result) {
7
+ if (result.error !== void 0 || !result.response.ok) {
8
+ throw result.error;
9
+ }
10
+ }
11
+ const DEFAULT_PAGE_PARAM_NAME = "cursor";
12
+ function pageInit(init, { pageParamName, pageParam, signal }) {
13
+ const { params, ...rest } = init ?? {};
14
+ const page = pageParam === void 0 ? {} : { [pageParamName]: pageParam };
15
+ return { ...rest, signal, params: { ...params, query: { ...params?.query, ...page } } };
16
+ }
17
+ function createClient(client) {
18
+ const queryFn = async ({
19
+ queryKey,
20
+ signal
21
+ }) => {
22
+ const [method, path, init] = queryKey;
23
+ const mth = method.toUpperCase();
24
+ const fn = client[mth];
25
+ const result = await fn(path, { signal, ...init });
26
+ throwIfFailed(result);
27
+ return result.data ?? null;
28
+ };
29
+ const queryOptions = (method, path, ...[init, options]) => ({
30
+ queryKey: init === void 0 ? [method, path] : [method, path, init],
31
+ queryFn,
32
+ ...options
33
+ });
34
+ return {
35
+ queryOptions,
36
+ useQuery: (method, path, ...[init, options, queryClient]) => reactQuery.useQuery(queryOptions(method, path, init, options), queryClient),
37
+ useSuspenseQuery: (method, path, ...[init, options, queryClient]) => reactQuery.useSuspenseQuery(queryOptions(method, path, init, options), queryClient),
38
+ useInfiniteQuery: (method, path, init, options, queryClient) => {
39
+ const { pageParamName = DEFAULT_PAGE_PARAM_NAME, placeholderData, ...restOptions } = options;
40
+ const { queryKey } = queryOptions(method, path, init);
41
+ const mth = method.toUpperCase();
42
+ const fn = client[mth];
43
+ return reactQuery.useInfiniteQuery(
44
+ {
45
+ queryKey: [...queryKey, INFINITE_QUERY_KEY_MARKER],
46
+ // The request comes from the arguments; the key only identifies the cache entry.
47
+ queryFn: async ({ pageParam, signal }) => {
48
+ const result = await fn(path, pageInit(init, { pageParamName, pageParam, signal }));
49
+ throwIfFailed(result);
50
+ return result.data;
51
+ },
52
+ ...restOptions,
53
+ // Typed with an `unknown` page param for the caller (`InfinitePlaceholderData`). TanStack
54
+ // passes the previous data of this query, with its own page params, and takes it back.
55
+ placeholderData
56
+ },
57
+ queryClient
58
+ );
59
+ },
60
+ useMutation: (method, path, options, queryClient) => reactQuery.useMutation(
61
+ {
62
+ mutationKey: [method, path],
63
+ mutationFn: async (init) => {
64
+ const mth = method.toUpperCase();
65
+ const fn = client[mth];
66
+ const result = await fn(path, init);
67
+ throwIfFailed(result);
68
+ const { data } = result;
69
+ return data;
70
+ },
71
+ ...options
72
+ },
73
+ queryClient
74
+ )
75
+ };
76
+ }
77
+
78
+ module.exports = createClient;
79
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/index.ts"],"sourcesContent":["import {\n type DataTag,\n type InfiniteData,\n type QueryClient,\n type QueryFunctionContext,\n type SkipToken,\n type UseInfiniteQueryOptions,\n type UseInfiniteQueryResult,\n type UseMutationOptions,\n type UseMutationResult,\n type UseQueryOptions,\n type UseQueryResult,\n type UseSuspenseQueryOptions,\n type UseSuspenseQueryResult,\n useInfiniteQuery,\n useMutation,\n useQuery,\n useSuspenseQuery,\n} from \"@tanstack/react-query\";\nimport type {\n ClientMethod,\n DefaultParamsOption,\n Client as FetchClient,\n FetchResponse,\n IsInitOptional,\n MaybeOptionalInit,\n PathOperation,\n} from \"openapi-typesafe-fetch\";\nimport type { HttpMethod, MediaType, PathsWithMethod } from \"openapi-typesafe-helpers\";\n\n// Helper type to dynamically infer the type from the `select` property\ntype InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;\n\n/**\n * Query data of a response type: a success without a body is `undefined` in the fetch client and\n * `null` in the query, as `queryFn` resolves it (upstream #2234, #2464).\n */\ntype QueryData<Data> = undefined extends Data ? Exclude<Data, undefined> | null : Data;\n\ntype InitWithUnknowns<Init> = Init & { [key: string]: unknown };\n\n/**\n * The arguments after the path: the init, required when the operation requires a parameter or a\n * body, then `Rest`. `IsInitOptional` keeps the init optional in\n * `Parameters<typeof $api.queryOptions>`, where it is the union of all inits (upstream #2178).\n */\ntype InitArguments<Init, Rest extends unknown[]> =\n IsInitOptional<Init> extends true ? [InitWithUnknowns<Init>?, ...Rest] : [InitWithUnknowns<Init>, ...Rest];\n\nexport type QueryKey<\n Paths extends {},\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init = MaybeOptionalInit<Paths[Path], Method>,\n> = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];\n\n/**\n * The last element of an infinite query's key. `useQuery` and `useInfiniteQuery` cache different\n * data (`TData` and `InfiniteData<TData>`), so the same request must not share one key (upstream\n * #2184, ADR-022). A prefix such as `[method, path]` still matches both. Not exported: a runtime\n * named export next to the default export would change what `require()` returns.\n */\nconst INFINITE_QUERY_KEY_MARKER = \"infinite\";\n\nexport type InfiniteQueryKey<\n Paths extends {},\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n> = readonly [...QueryKey<Paths, Method, Path>, typeof INFINITE_QUERY_KEY_MARKER];\n\nexport type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types\n Options extends Omit<\n UseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >,\n \"queryKey\" | \"queryFn\"\n >,\n>(\n method: Method,\n path: Path,\n ...[init, options]: InitArguments<Init, [Options?]>\n) => NoInfer<\n Omit<\n UseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >,\n \"queryFn\" | \"queryKey\"\n > & {\n queryKey: DataTag<QueryKey<Paths, Method, Path>, QueryData<Response[\"data\"]>, Response[\"error\"]>;\n queryFn: Exclude<\n UseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >[\"queryFn\"],\n SkipToken | undefined\n >;\n }\n>;\n\nexport type UseQueryMethod<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types\n Options extends Omit<\n UseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >,\n \"queryKey\" | \"queryFn\"\n >,\n>(\n method: Method,\n url: Path,\n ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>\n) => UseQueryResult<InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>, Response[\"error\"]>;\n\n/**\n * The type of the page params: the initial one and the ones `getNextPageParam` and\n * `getPreviousPageParam` return (upstream #2458). It was `unknown`. It stays `unknown` when a\n * return type is untyped, so code that compiled still compiles (roadmap §3): TypeScript cannot\n * infer the return type of a callback that computes it from an unannotated `lastPageParam`,\n * whose type depends on that return type, and gives `never` or `unknown`.\n */\ntype PageParam<Initial, Next, Previous> =\n IsUntyped<Next> extends true\n ? unknown\n : [Previous] extends [NoPreviousPageParam]\n ? NullishToUnknown<Initial | NonNullable<Next>>\n : IsUntyped<Previous> extends true\n ? unknown\n : NullishToUnknown<Initial | NonNullable<Next> | NonNullable<Previous>>;\n\n/** The `Previous` type parameter when the options have no `getPreviousPageParam`. */\ndeclare const NO_PREVIOUS_PAGE_PARAM: unique symbol;\ntype NoPreviousPageParam = typeof NO_PREVIOUS_PAGE_PARAM;\n\ntype IsUntyped<T> = [T] extends [never] ? true : unknown extends T ? true : false;\n\n/** Only `undefined` or `null` names no type: a cursor API's `initialPageParam: undefined`. */\ntype NullishToUnknown<T> = [T] extends [undefined | null] ? unknown : T;\n\n/** Read only for the return type; `never` arguments add no inference for TanStack's page param. */\ntype PageParamFunction<Result> = (...args: never) => Result;\n\n/**\n * Inference sites of the page param types. Intersected before `Options`, so TanStack's own\n * inference in the implementation reads the full signatures of `Options`.\n */\ninterface PageParamSources<Initial, Next, Previous> {\n initialPageParam: Initial;\n getNextPageParam: PageParamFunction<Next>;\n getPreviousPageParam?: PageParamFunction<Previous>;\n}\n\n/**\n * `placeholderData` of `useInfiniteQuery`, with an `unknown` page param as before: TypeScript types\n * a `(previousData) => previousData` callback while it still infers the page param, so it got\n * `unknown` there and failed against the typed page param.\n */\ntype InfinitePlaceholderData<\n Data,\n Error,\n Paths extends {},\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n> = UseInfiniteQueryOptions<\n Data,\n Error,\n InfiniteData<Data, unknown>,\n InfiniteQueryKey<Paths, Method, Path>,\n unknown\n>[\"placeholderData\"];\n\nexport type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>,\n Options extends Omit<\n UseInfiniteQueryOptions<\n Response[\"data\"],\n Response[\"error\"],\n InferSelectReturnType<InfiniteData<Response[\"data\"], PageParam<Initial, Next, Previous>>, Options[\"select\"]>,\n InfiniteQueryKey<Paths, Method, Path>,\n PageParam<Initial, Next, Previous>\n >,\n \"queryKey\" | \"queryFn\" | \"placeholderData\"\n > & {\n placeholderData?: InfinitePlaceholderData<Response[\"data\"], Response[\"error\"], Paths, Method, Path>;\n pageParamName?: string;\n },\n Initial = unknown,\n Next = never,\n Previous = NoPreviousPageParam,\n>(\n method: Method,\n url: Path,\n init: InitWithUnknowns<Init>,\n options: PageParamSources<Initial, Next, Previous> & Options,\n queryClient?: QueryClient,\n) => UseInfiniteQueryResult<\n InferSelectReturnType<InfiniteData<Response[\"data\"], PageParam<Initial, Next, Previous>>, Options[\"select\"]>,\n Response[\"error\"]\n>;\n\nexport type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types\n Options extends Omit<\n UseSuspenseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >,\n \"queryKey\" | \"queryFn\"\n >,\n>(\n method: Method,\n url: Path,\n ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>\n) => UseSuspenseQueryResult<InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>, Response[\"error\"]>;\n\nexport type UseMutationMethod<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types\n TOnMutateResult = unknown,\n>(\n method: Method,\n url: Path,\n options?: Omit<\n UseMutationOptions<Response[\"data\"], Response[\"error\"], Init, TOnMutateResult>,\n \"mutationKey\" | \"mutationFn\"\n >,\n queryClient?: QueryClient,\n) => UseMutationResult<Response[\"data\"], Response[\"error\"], Init, TOnMutateResult>;\n\nexport interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {\n queryOptions: QueryOptionsFunction<Paths, Media>;\n useQuery: UseQueryMethod<Paths, Media>;\n useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;\n useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;\n useMutation: UseMutationMethod<Paths, Media>;\n}\n\nexport type MethodResponse<\n CreatedClient extends OpenapiQueryClient<any, any>,\n Method extends HttpMethod,\n Path extends CreatedClient extends OpenapiQueryClient<infer Paths, infer _Media>\n ? PathsWithMethod<Paths, Method>\n : never,\n Options = object,\n> =\n CreatedClient extends OpenapiQueryClient<infer Paths extends { [key: string]: any }, infer Media extends MediaType>\n ? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>[\"data\"]>\n : never;\n\n/**\n * Throw the error of a failed fetch result. `if (error)` missed two failures: a non-OK response with\n * an empty body (`error` is `undefined`, upstream #2070, #2829) and a falsy error body such as `0`.\n */\nfunction throwIfFailed(result: { error?: unknown; response: Response }): void {\n if (result.error !== undefined || !result.response.ok) {\n throw result.error;\n }\n}\n\n/** The query parameter that carries the page param unless `pageParamName` names another. */\nconst DEFAULT_PAGE_PARAM_NAME = \"cursor\";\n\ninterface PageRequest {\n readonly pageParamName: string;\n readonly pageParam: unknown;\n readonly signal: AbortSignal;\n}\n\n/**\n * The request init of one page. An `undefined` page param (the first page with\n * `initialPageParam: undefined`) is left out instead of being sent as `0` (upstream #2715, #2340,\n * ADR-022), so a cursor API gets no cursor on the first page.\n */\nfunction pageInit(init: unknown, { pageParamName, pageParam, signal }: PageRequest) {\n const { params, ...rest } = (init ?? {}) as { params?: { query?: DefaultParamsOption } };\n const page = pageParam === undefined ? {} : { [pageParamName]: pageParam };\n return { ...rest, signal, params: { ...params, query: { ...params?.query, ...page } } };\n}\n\n// TODO: Add the ability to bring queryClient as argument\nexport default function createClient<Paths extends {}, Media extends MediaType = MediaType>(\n client: FetchClient<Paths, Media>,\n): OpenapiQueryClient<Paths, Media> {\n const queryFn = async <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>>({\n queryKey,\n signal,\n }: QueryFunctionContext<QueryKey<Paths, Method, Path>>) => {\n // While `Path` is generic, TypeScript cannot work out that the key is a tuple: the init is a\n // conditional type per operation (`MaybeOptionalInit`), and the key's type waits for it.\n const [method, path, init] = queryKey as readonly [Method, Path, unknown?];\n const mth = method.toUpperCase() as Uppercase<typeof method>;\n const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;\n const result = await fn(path, { signal, ...(init as any) }); // TODO: find a way to avoid as any\n throwIfFailed(result);\n // TanStack Query rejects `undefined` query data. The fetch client gives `undefined` for every\n // success without a body: a 204, `Content-Length: 0`, an empty chunked body, a HEAD request\n // (upstream #2234). A parsed body is never `undefined`, so this changes nothing else.\n return result.data ?? null;\n };\n\n const queryOptions: QueryOptionsFunction<Paths, Media> = (method, path, ...[init, options]) => ({\n queryKey: (init === undefined ? ([method, path] as const) : ([method, path, init] as const)) as DataTag<\n QueryKey<Paths, typeof method, typeof path>,\n any,\n any\n >,\n queryFn,\n ...options,\n });\n\n return {\n queryOptions,\n useQuery: (method, path, ...[init, options, queryClient]) =>\n useQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),\n useSuspenseQuery: (method, path, ...[init, options, queryClient]) =>\n useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),\n useInfiniteQuery: (method, path, init, options, queryClient) => {\n const { pageParamName = DEFAULT_PAGE_PARAM_NAME, placeholderData, ...restOptions } = options;\n const { queryKey } = queryOptions(method, path, init);\n const mth = method.toUpperCase() as Uppercase<typeof method>;\n const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;\n return useInfiniteQuery(\n {\n queryKey: [...queryKey, INFINITE_QUERY_KEY_MARKER] as const,\n // The request comes from the arguments; the key only identifies the cache entry.\n queryFn: async ({ pageParam, signal }) => {\n const result = await fn(path, pageInit(init, { pageParamName, pageParam, signal }) as any);\n throwIfFailed(result);\n // An empty page stays `undefined`, as in upstream: TanStack Query rejects only\n // `undefined` query data, and the infinite query data is the `{ pages }` object.\n return result.data;\n },\n ...restOptions,\n // Typed with an `unknown` page param for the caller (`InfinitePlaceholderData`). TanStack\n // passes the previous data of this query, with its own page params, and takes it back.\n placeholderData: placeholderData as any,\n },\n queryClient,\n );\n },\n useMutation: (method, path, options, queryClient) =>\n useMutation(\n {\n mutationKey: [method, path],\n mutationFn: async (init) => {\n const mth = method.toUpperCase() as Uppercase<typeof method>;\n const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;\n const result = await fn(path, init as InitWithUnknowns<typeof init>);\n throwIfFailed(result);\n // TanStack Query accepts `undefined` mutation data, so a success without a body stays\n // `undefined`, as in upstream; only query data needs `null` (upstream #2234). The cast\n // only bridges the generic types: `Response[\"data\"]` still has `undefined` for an\n // operation with a response without a body.\n const { data } = result;\n return data as Exclude<typeof data, undefined>;\n },\n ...options,\n },\n queryClient,\n ),\n };\n}\n"],"names":["useQuery","useSuspenseQuery","useInfiniteQuery","useMutation"],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { UseQueryOptions, DataTag, SkipToken, QueryClient, UseQueryResult, UseSuspenseQueryOptions, UseSuspenseQueryResult, UseInfiniteQueryOptions, InfiniteData, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
2
+ import { MaybeOptionalInit, FetchResponse, PathOperation, IsInitOptional, Client } from 'openapi-typesafe-fetch';
3
+ import { HttpMethod, PathsWithMethod, MediaType } from 'openapi-typesafe-helpers';
4
+
5
+ type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
6
+ type QueryData<Data> = undefined extends Data ? Exclude<Data, undefined> | null : Data;
7
+ type InitWithUnknowns<Init> = Init & {
8
+ [key: string]: unknown;
9
+ };
10
+ type InitArguments<Init, Rest extends unknown[]> = IsInitOptional<Init> extends true ? [InitWithUnknowns<Init>?, ...Rest] : [InitWithUnknowns<Init>, ...Rest];
11
+ type QueryKey<Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init = MaybeOptionalInit<Paths[Path], Method>> = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];
12
+ declare const INFINITE_QUERY_KEY_MARKER = "infinite";
13
+ type InfiniteQueryKey<Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>> = readonly [...QueryKey<Paths, Method, Path>, typeof INFINITE_QUERY_KEY_MARKER];
14
+ type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, path: Path, ...[init, options]: InitArguments<Init, [Options?]>) => NoInfer<Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryFn" | "queryKey"> & {
15
+ queryKey: DataTag<QueryKey<Paths, Method, Path>, QueryData<Response["data"]>, Response["error"]>;
16
+ queryFn: Exclude<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>["queryFn"], SkipToken | undefined>;
17
+ }>;
18
+ type UseQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, url: Path, ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>) => UseQueryResult<InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, Response["error"]>;
19
+ type PageParam<Initial, Next, Previous> = IsUntyped<Next> extends true ? unknown : [Previous] extends [NoPreviousPageParam] ? NullishToUnknown<Initial | NonNullable<Next>> : IsUntyped<Previous> extends true ? unknown : NullishToUnknown<Initial | NonNullable<Next> | NonNullable<Previous>>;
20
+ declare const NO_PREVIOUS_PAGE_PARAM: unique symbol;
21
+ type NoPreviousPageParam = typeof NO_PREVIOUS_PAGE_PARAM;
22
+ type IsUntyped<T> = [T] extends [never] ? true : unknown extends T ? true : false;
23
+ type NullishToUnknown<T> = [T] extends [undefined | null] ? unknown : T;
24
+ type PageParamFunction<Result> = (...args: never) => Result;
25
+ interface PageParamSources<Initial, Next, Previous> {
26
+ initialPageParam: Initial;
27
+ getNextPageParam: PageParamFunction<Next>;
28
+ getPreviousPageParam?: PageParamFunction<Previous>;
29
+ }
30
+ type InfinitePlaceholderData<Data, Error, Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>> = UseInfiniteQueryOptions<Data, Error, InfiniteData<Data, unknown>, InfiniteQueryKey<Paths, Method, Path>, unknown>["placeholderData"];
31
+ type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseInfiniteQueryOptions<Response["data"], Response["error"], InferSelectReturnType<InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>, Options["select"]>, InfiniteQueryKey<Paths, Method, Path>, PageParam<Initial, Next, Previous>>, "queryKey" | "queryFn" | "placeholderData"> & {
32
+ placeholderData?: InfinitePlaceholderData<Response["data"], Response["error"], Paths, Method, Path>;
33
+ pageParamName?: string;
34
+ }, Initial = unknown, Next = never, Previous = NoPreviousPageParam>(method: Method, url: Path, init: InitWithUnknowns<Init>, options: PageParamSources<Initial, Next, Previous> & Options, queryClient?: QueryClient) => UseInfiniteQueryResult<InferSelectReturnType<InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>, Options["select"]>, Response["error"]>;
35
+ type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseSuspenseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, url: Path, ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>) => UseSuspenseQueryResult<InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, Response["error"]>;
36
+ type UseMutationMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, TOnMutateResult = unknown>(method: Method, url: Path, options?: Omit<UseMutationOptions<Response["data"], Response["error"], Init, TOnMutateResult>, "mutationKey" | "mutationFn">, queryClient?: QueryClient) => UseMutationResult<Response["data"], Response["error"], Init, TOnMutateResult>;
37
+ interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
38
+ queryOptions: QueryOptionsFunction<Paths, Media>;
39
+ useQuery: UseQueryMethod<Paths, Media>;
40
+ useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
41
+ useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;
42
+ useMutation: UseMutationMethod<Paths, Media>;
43
+ }
44
+ type MethodResponse<CreatedClient extends OpenapiQueryClient<any, any>, Method extends HttpMethod, Path extends CreatedClient extends OpenapiQueryClient<infer Paths, infer _Media> ? PathsWithMethod<Paths, Method> : never, Options = object> = CreatedClient extends OpenapiQueryClient<infer Paths extends {
45
+ [key: string]: any;
46
+ }, infer Media extends MediaType> ? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>["data"]> : never;
47
+ declare function createClient<Paths extends {}, Media extends MediaType = MediaType>(client: Client<Paths, Media>): OpenapiQueryClient<Paths, Media>;
48
+
49
+ declare namespace createClient {
50
+ export { InfiniteQueryKey, MethodResponse, OpenapiQueryClient, QueryKey, QueryOptionsFunction, UseInfiniteQueryMethod, UseMutationMethod, UseQueryMethod, UseSuspenseQueryMethod };
51
+ }
52
+ export = createClient;
@@ -0,0 +1,50 @@
1
+ import { UseQueryOptions, DataTag, SkipToken, QueryClient, UseQueryResult, UseSuspenseQueryOptions, UseSuspenseQueryResult, UseInfiniteQueryOptions, InfiniteData, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
2
+ import { MaybeOptionalInit, FetchResponse, PathOperation, IsInitOptional, Client } from 'openapi-typesafe-fetch';
3
+ import { HttpMethod, PathsWithMethod, MediaType } from 'openapi-typesafe-helpers';
4
+
5
+ type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
6
+ type QueryData<Data> = undefined extends Data ? Exclude<Data, undefined> | null : Data;
7
+ type InitWithUnknowns<Init> = Init & {
8
+ [key: string]: unknown;
9
+ };
10
+ type InitArguments<Init, Rest extends unknown[]> = IsInitOptional<Init> extends true ? [InitWithUnknowns<Init>?, ...Rest] : [InitWithUnknowns<Init>, ...Rest];
11
+ type QueryKey<Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init = MaybeOptionalInit<Paths[Path], Method>> = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];
12
+ declare const INFINITE_QUERY_KEY_MARKER = "infinite";
13
+ type InfiniteQueryKey<Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>> = readonly [...QueryKey<Paths, Method, Path>, typeof INFINITE_QUERY_KEY_MARKER];
14
+ type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, path: Path, ...[init, options]: InitArguments<Init, [Options?]>) => NoInfer<Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryFn" | "queryKey"> & {
15
+ queryKey: DataTag<QueryKey<Paths, Method, Path>, QueryData<Response["data"]>, Response["error"]>;
16
+ queryFn: Exclude<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>["queryFn"], SkipToken | undefined>;
17
+ }>;
18
+ type UseQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, url: Path, ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>) => UseQueryResult<InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, Response["error"]>;
19
+ type PageParam<Initial, Next, Previous> = IsUntyped<Next> extends true ? unknown : [Previous] extends [NoPreviousPageParam] ? NullishToUnknown<Initial | NonNullable<Next>> : IsUntyped<Previous> extends true ? unknown : NullishToUnknown<Initial | NonNullable<Next> | NonNullable<Previous>>;
20
+ declare const NO_PREVIOUS_PAGE_PARAM: unique symbol;
21
+ type NoPreviousPageParam = typeof NO_PREVIOUS_PAGE_PARAM;
22
+ type IsUntyped<T> = [T] extends [never] ? true : unknown extends T ? true : false;
23
+ type NullishToUnknown<T> = [T] extends [undefined | null] ? unknown : T;
24
+ type PageParamFunction<Result> = (...args: never) => Result;
25
+ interface PageParamSources<Initial, Next, Previous> {
26
+ initialPageParam: Initial;
27
+ getNextPageParam: PageParamFunction<Next>;
28
+ getPreviousPageParam?: PageParamFunction<Previous>;
29
+ }
30
+ type InfinitePlaceholderData<Data, Error, Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>> = UseInfiniteQueryOptions<Data, Error, InfiniteData<Data, unknown>, InfiniteQueryKey<Paths, Method, Path>, unknown>["placeholderData"];
31
+ type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseInfiniteQueryOptions<Response["data"], Response["error"], InferSelectReturnType<InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>, Options["select"]>, InfiniteQueryKey<Paths, Method, Path>, PageParam<Initial, Next, Previous>>, "queryKey" | "queryFn" | "placeholderData"> & {
32
+ placeholderData?: InfinitePlaceholderData<Response["data"], Response["error"], Paths, Method, Path>;
33
+ pageParamName?: string;
34
+ }, Initial = unknown, Next = never, Previous = NoPreviousPageParam>(method: Method, url: Path, init: InitWithUnknowns<Init>, options: PageParamSources<Initial, Next, Previous> & Options, queryClient?: QueryClient) => UseInfiniteQueryResult<InferSelectReturnType<InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>, Options["select"]>, Response["error"]>;
35
+ type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseSuspenseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, url: Path, ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>) => UseSuspenseQueryResult<InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, Response["error"]>;
36
+ type UseMutationMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, TOnMutateResult = unknown>(method: Method, url: Path, options?: Omit<UseMutationOptions<Response["data"], Response["error"], Init, TOnMutateResult>, "mutationKey" | "mutationFn">, queryClient?: QueryClient) => UseMutationResult<Response["data"], Response["error"], Init, TOnMutateResult>;
37
+ interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
38
+ queryOptions: QueryOptionsFunction<Paths, Media>;
39
+ useQuery: UseQueryMethod<Paths, Media>;
40
+ useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
41
+ useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;
42
+ useMutation: UseMutationMethod<Paths, Media>;
43
+ }
44
+ type MethodResponse<CreatedClient extends OpenapiQueryClient<any, any>, Method extends HttpMethod, Path extends CreatedClient extends OpenapiQueryClient<infer Paths, infer _Media> ? PathsWithMethod<Paths, Method> : never, Options = object> = CreatedClient extends OpenapiQueryClient<infer Paths extends {
45
+ [key: string]: any;
46
+ }, infer Media extends MediaType> ? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>["data"]> : never;
47
+ declare function createClient<Paths extends {}, Media extends MediaType = MediaType>(client: Client<Paths, Media>): OpenapiQueryClient<Paths, Media>;
48
+
49
+ export { createClient as default };
50
+ export type { InfiniteQueryKey, MethodResponse, OpenapiQueryClient, QueryKey, QueryOptionsFunction, UseInfiniteQueryMethod, UseMutationMethod, UseQueryMethod, UseSuspenseQueryMethod };
@@ -0,0 +1,50 @@
1
+ import { UseQueryOptions, DataTag, SkipToken, QueryClient, UseQueryResult, UseSuspenseQueryOptions, UseSuspenseQueryResult, UseInfiniteQueryOptions, InfiniteData, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
2
+ import { MaybeOptionalInit, FetchResponse, PathOperation, IsInitOptional, Client } from 'openapi-typesafe-fetch';
3
+ import { HttpMethod, PathsWithMethod, MediaType } from 'openapi-typesafe-helpers';
4
+
5
+ type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
6
+ type QueryData<Data> = undefined extends Data ? Exclude<Data, undefined> | null : Data;
7
+ type InitWithUnknowns<Init> = Init & {
8
+ [key: string]: unknown;
9
+ };
10
+ type InitArguments<Init, Rest extends unknown[]> = IsInitOptional<Init> extends true ? [InitWithUnknowns<Init>?, ...Rest] : [InitWithUnknowns<Init>, ...Rest];
11
+ type QueryKey<Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init = MaybeOptionalInit<Paths[Path], Method>> = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];
12
+ declare const INFINITE_QUERY_KEY_MARKER = "infinite";
13
+ type InfiniteQueryKey<Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>> = readonly [...QueryKey<Paths, Method, Path>, typeof INFINITE_QUERY_KEY_MARKER];
14
+ type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, path: Path, ...[init, options]: InitArguments<Init, [Options?]>) => NoInfer<Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryFn" | "queryKey"> & {
15
+ queryKey: DataTag<QueryKey<Paths, Method, Path>, QueryData<Response["data"]>, Response["error"]>;
16
+ queryFn: Exclude<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>["queryFn"], SkipToken | undefined>;
17
+ }>;
18
+ type UseQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, url: Path, ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>) => UseQueryResult<InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, Response["error"]>;
19
+ type PageParam<Initial, Next, Previous> = IsUntyped<Next> extends true ? unknown : [Previous] extends [NoPreviousPageParam] ? NullishToUnknown<Initial | NonNullable<Next>> : IsUntyped<Previous> extends true ? unknown : NullishToUnknown<Initial | NonNullable<Next> | NonNullable<Previous>>;
20
+ declare const NO_PREVIOUS_PAGE_PARAM: unique symbol;
21
+ type NoPreviousPageParam = typeof NO_PREVIOUS_PAGE_PARAM;
22
+ type IsUntyped<T> = [T] extends [never] ? true : unknown extends T ? true : false;
23
+ type NullishToUnknown<T> = [T] extends [undefined | null] ? unknown : T;
24
+ type PageParamFunction<Result> = (...args: never) => Result;
25
+ interface PageParamSources<Initial, Next, Previous> {
26
+ initialPageParam: Initial;
27
+ getNextPageParam: PageParamFunction<Next>;
28
+ getPreviousPageParam?: PageParamFunction<Previous>;
29
+ }
30
+ type InfinitePlaceholderData<Data, Error, Paths extends {}, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>> = UseInfiniteQueryOptions<Data, Error, InfiniteData<Data, unknown>, InfiniteQueryKey<Paths, Method, Path>, unknown>["placeholderData"];
31
+ type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseInfiniteQueryOptions<Response["data"], Response["error"], InferSelectReturnType<InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>, Options["select"]>, InfiniteQueryKey<Paths, Method, Path>, PageParam<Initial, Next, Previous>>, "queryKey" | "queryFn" | "placeholderData"> & {
32
+ placeholderData?: InfinitePlaceholderData<Response["data"], Response["error"], Paths, Method, Path>;
33
+ pageParamName?: string;
34
+ }, Initial = unknown, Next = never, Previous = NoPreviousPageParam>(method: Method, url: Path, init: InitWithUnknowns<Init>, options: PageParamSources<Initial, Next, Previous> & Options, queryClient?: QueryClient) => UseInfiniteQueryResult<InferSelectReturnType<InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>, Options["select"]>, Response["error"]>;
35
+ type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, Options extends Omit<UseSuspenseQueryOptions<QueryData<Response["data"]>, Response["error"], InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(method: Method, url: Path, ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>) => UseSuspenseQueryResult<InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, Response["error"]>;
36
+ type UseMutationMethod<Paths extends {}, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, TOnMutateResult = unknown>(method: Method, url: Path, options?: Omit<UseMutationOptions<Response["data"], Response["error"], Init, TOnMutateResult>, "mutationKey" | "mutationFn">, queryClient?: QueryClient) => UseMutationResult<Response["data"], Response["error"], Init, TOnMutateResult>;
37
+ interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
38
+ queryOptions: QueryOptionsFunction<Paths, Media>;
39
+ useQuery: UseQueryMethod<Paths, Media>;
40
+ useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
41
+ useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;
42
+ useMutation: UseMutationMethod<Paths, Media>;
43
+ }
44
+ type MethodResponse<CreatedClient extends OpenapiQueryClient<any, any>, Method extends HttpMethod, Path extends CreatedClient extends OpenapiQueryClient<infer Paths, infer _Media> ? PathsWithMethod<Paths, Method> : never, Options = object> = CreatedClient extends OpenapiQueryClient<infer Paths extends {
45
+ [key: string]: any;
46
+ }, infer Media extends MediaType> ? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>["data"]> : never;
47
+ declare function createClient<Paths extends {}, Media extends MediaType = MediaType>(client: Client<Paths, Media>): OpenapiQueryClient<Paths, Media>;
48
+
49
+ export { createClient as default };
50
+ export type { InfiniteQueryKey, MethodResponse, OpenapiQueryClient, QueryKey, QueryOptionsFunction, UseInfiniteQueryMethod, UseMutationMethod, UseQueryMethod, UseSuspenseQueryMethod };
package/dist/index.mjs ADDED
@@ -0,0 +1,77 @@
1
+ import { useMutation, useInfiniteQuery, useSuspenseQuery, useQuery } from '@tanstack/react-query';
2
+
3
+ const INFINITE_QUERY_KEY_MARKER = "infinite";
4
+ function throwIfFailed(result) {
5
+ if (result.error !== void 0 || !result.response.ok) {
6
+ throw result.error;
7
+ }
8
+ }
9
+ const DEFAULT_PAGE_PARAM_NAME = "cursor";
10
+ function pageInit(init, { pageParamName, pageParam, signal }) {
11
+ const { params, ...rest } = init ?? {};
12
+ const page = pageParam === void 0 ? {} : { [pageParamName]: pageParam };
13
+ return { ...rest, signal, params: { ...params, query: { ...params?.query, ...page } } };
14
+ }
15
+ function createClient(client) {
16
+ const queryFn = async ({
17
+ queryKey,
18
+ signal
19
+ }) => {
20
+ const [method, path, init] = queryKey;
21
+ const mth = method.toUpperCase();
22
+ const fn = client[mth];
23
+ const result = await fn(path, { signal, ...init });
24
+ throwIfFailed(result);
25
+ return result.data ?? null;
26
+ };
27
+ const queryOptions = (method, path, ...[init, options]) => ({
28
+ queryKey: init === void 0 ? [method, path] : [method, path, init],
29
+ queryFn,
30
+ ...options
31
+ });
32
+ return {
33
+ queryOptions,
34
+ useQuery: (method, path, ...[init, options, queryClient]) => useQuery(queryOptions(method, path, init, options), queryClient),
35
+ useSuspenseQuery: (method, path, ...[init, options, queryClient]) => useSuspenseQuery(queryOptions(method, path, init, options), queryClient),
36
+ useInfiniteQuery: (method, path, init, options, queryClient) => {
37
+ const { pageParamName = DEFAULT_PAGE_PARAM_NAME, placeholderData, ...restOptions } = options;
38
+ const { queryKey } = queryOptions(method, path, init);
39
+ const mth = method.toUpperCase();
40
+ const fn = client[mth];
41
+ return useInfiniteQuery(
42
+ {
43
+ queryKey: [...queryKey, INFINITE_QUERY_KEY_MARKER],
44
+ // The request comes from the arguments; the key only identifies the cache entry.
45
+ queryFn: async ({ pageParam, signal }) => {
46
+ const result = await fn(path, pageInit(init, { pageParamName, pageParam, signal }));
47
+ throwIfFailed(result);
48
+ return result.data;
49
+ },
50
+ ...restOptions,
51
+ // Typed with an `unknown` page param for the caller (`InfinitePlaceholderData`). TanStack
52
+ // passes the previous data of this query, with its own page params, and takes it back.
53
+ placeholderData
54
+ },
55
+ queryClient
56
+ );
57
+ },
58
+ useMutation: (method, path, options, queryClient) => useMutation(
59
+ {
60
+ mutationKey: [method, path],
61
+ mutationFn: async (init) => {
62
+ const mth = method.toUpperCase();
63
+ const fn = client[mth];
64
+ const result = await fn(path, init);
65
+ throwIfFailed(result);
66
+ const { data } = result;
67
+ return data;
68
+ },
69
+ ...options
70
+ },
71
+ queryClient
72
+ )
73
+ };
74
+ }
75
+
76
+ export { createClient as default };
77
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":["import {\n type DataTag,\n type InfiniteData,\n type QueryClient,\n type QueryFunctionContext,\n type SkipToken,\n type UseInfiniteQueryOptions,\n type UseInfiniteQueryResult,\n type UseMutationOptions,\n type UseMutationResult,\n type UseQueryOptions,\n type UseQueryResult,\n type UseSuspenseQueryOptions,\n type UseSuspenseQueryResult,\n useInfiniteQuery,\n useMutation,\n useQuery,\n useSuspenseQuery,\n} from \"@tanstack/react-query\";\nimport type {\n ClientMethod,\n DefaultParamsOption,\n Client as FetchClient,\n FetchResponse,\n IsInitOptional,\n MaybeOptionalInit,\n PathOperation,\n} from \"openapi-typesafe-fetch\";\nimport type { HttpMethod, MediaType, PathsWithMethod } from \"openapi-typesafe-helpers\";\n\n// Helper type to dynamically infer the type from the `select` property\ntype InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;\n\n/**\n * Query data of a response type: a success without a body is `undefined` in the fetch client and\n * `null` in the query, as `queryFn` resolves it (upstream #2234, #2464).\n */\ntype QueryData<Data> = undefined extends Data ? Exclude<Data, undefined> | null : Data;\n\ntype InitWithUnknowns<Init> = Init & { [key: string]: unknown };\n\n/**\n * The arguments after the path: the init, required when the operation requires a parameter or a\n * body, then `Rest`. `IsInitOptional` keeps the init optional in\n * `Parameters<typeof $api.queryOptions>`, where it is the union of all inits (upstream #2178).\n */\ntype InitArguments<Init, Rest extends unknown[]> =\n IsInitOptional<Init> extends true ? [InitWithUnknowns<Init>?, ...Rest] : [InitWithUnknowns<Init>, ...Rest];\n\nexport type QueryKey<\n Paths extends {},\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init = MaybeOptionalInit<Paths[Path], Method>,\n> = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];\n\n/**\n * The last element of an infinite query's key. `useQuery` and `useInfiniteQuery` cache different\n * data (`TData` and `InfiniteData<TData>`), so the same request must not share one key (upstream\n * #2184, ADR-022). A prefix such as `[method, path]` still matches both. Not exported: a runtime\n * named export next to the default export would change what `require()` returns.\n */\nconst INFINITE_QUERY_KEY_MARKER = \"infinite\";\n\nexport type InfiniteQueryKey<\n Paths extends {},\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n> = readonly [...QueryKey<Paths, Method, Path>, typeof INFINITE_QUERY_KEY_MARKER];\n\nexport type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types\n Options extends Omit<\n UseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >,\n \"queryKey\" | \"queryFn\"\n >,\n>(\n method: Method,\n path: Path,\n ...[init, options]: InitArguments<Init, [Options?]>\n) => NoInfer<\n Omit<\n UseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >,\n \"queryFn\" | \"queryKey\"\n > & {\n queryKey: DataTag<QueryKey<Paths, Method, Path>, QueryData<Response[\"data\"]>, Response[\"error\"]>;\n queryFn: Exclude<\n UseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >[\"queryFn\"],\n SkipToken | undefined\n >;\n }\n>;\n\nexport type UseQueryMethod<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types\n Options extends Omit<\n UseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >,\n \"queryKey\" | \"queryFn\"\n >,\n>(\n method: Method,\n url: Path,\n ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>\n) => UseQueryResult<InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>, Response[\"error\"]>;\n\n/**\n * The type of the page params: the initial one and the ones `getNextPageParam` and\n * `getPreviousPageParam` return (upstream #2458). It was `unknown`. It stays `unknown` when a\n * return type is untyped, so code that compiled still compiles (roadmap §3): TypeScript cannot\n * infer the return type of a callback that computes it from an unannotated `lastPageParam`,\n * whose type depends on that return type, and gives `never` or `unknown`.\n */\ntype PageParam<Initial, Next, Previous> =\n IsUntyped<Next> extends true\n ? unknown\n : [Previous] extends [NoPreviousPageParam]\n ? NullishToUnknown<Initial | NonNullable<Next>>\n : IsUntyped<Previous> extends true\n ? unknown\n : NullishToUnknown<Initial | NonNullable<Next> | NonNullable<Previous>>;\n\n/** The `Previous` type parameter when the options have no `getPreviousPageParam`. */\ndeclare const NO_PREVIOUS_PAGE_PARAM: unique symbol;\ntype NoPreviousPageParam = typeof NO_PREVIOUS_PAGE_PARAM;\n\ntype IsUntyped<T> = [T] extends [never] ? true : unknown extends T ? true : false;\n\n/** Only `undefined` or `null` names no type: a cursor API's `initialPageParam: undefined`. */\ntype NullishToUnknown<T> = [T] extends [undefined | null] ? unknown : T;\n\n/** Read only for the return type; `never` arguments add no inference for TanStack's page param. */\ntype PageParamFunction<Result> = (...args: never) => Result;\n\n/**\n * Inference sites of the page param types. Intersected before `Options`, so TanStack's own\n * inference in the implementation reads the full signatures of `Options`.\n */\ninterface PageParamSources<Initial, Next, Previous> {\n initialPageParam: Initial;\n getNextPageParam: PageParamFunction<Next>;\n getPreviousPageParam?: PageParamFunction<Previous>;\n}\n\n/**\n * `placeholderData` of `useInfiniteQuery`, with an `unknown` page param as before: TypeScript types\n * a `(previousData) => previousData` callback while it still infers the page param, so it got\n * `unknown` there and failed against the typed page param.\n */\ntype InfinitePlaceholderData<\n Data,\n Error,\n Paths extends {},\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n> = UseInfiniteQueryOptions<\n Data,\n Error,\n InfiniteData<Data, unknown>,\n InfiniteQueryKey<Paths, Method, Path>,\n unknown\n>[\"placeholderData\"];\n\nexport type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>,\n Options extends Omit<\n UseInfiniteQueryOptions<\n Response[\"data\"],\n Response[\"error\"],\n InferSelectReturnType<InfiniteData<Response[\"data\"], PageParam<Initial, Next, Previous>>, Options[\"select\"]>,\n InfiniteQueryKey<Paths, Method, Path>,\n PageParam<Initial, Next, Previous>\n >,\n \"queryKey\" | \"queryFn\" | \"placeholderData\"\n > & {\n placeholderData?: InfinitePlaceholderData<Response[\"data\"], Response[\"error\"], Paths, Method, Path>;\n pageParamName?: string;\n },\n Initial = unknown,\n Next = never,\n Previous = NoPreviousPageParam,\n>(\n method: Method,\n url: Path,\n init: InitWithUnknowns<Init>,\n options: PageParamSources<Initial, Next, Previous> & Options,\n queryClient?: QueryClient,\n) => UseInfiniteQueryResult<\n InferSelectReturnType<InfiniteData<Response[\"data\"], PageParam<Initial, Next, Previous>>, Options[\"select\"]>,\n Response[\"error\"]\n>;\n\nexport type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types\n Options extends Omit<\n UseSuspenseQueryOptions<\n QueryData<Response[\"data\"]>,\n Response[\"error\"],\n InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>,\n QueryKey<Paths, Method, Path>\n >,\n \"queryKey\" | \"queryFn\"\n >,\n>(\n method: Method,\n url: Path,\n ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>\n) => UseSuspenseQueryResult<InferSelectReturnType<QueryData<Response[\"data\"]>, Options[\"select\"]>, Response[\"error\"]>;\n\nexport type UseMutationMethod<Paths extends {}, Media extends MediaType> = <\n Method extends HttpMethod,\n Path extends PathsWithMethod<Paths, Method>,\n Init extends MaybeOptionalInit<Paths[Path], Method>,\n Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types\n TOnMutateResult = unknown,\n>(\n method: Method,\n url: Path,\n options?: Omit<\n UseMutationOptions<Response[\"data\"], Response[\"error\"], Init, TOnMutateResult>,\n \"mutationKey\" | \"mutationFn\"\n >,\n queryClient?: QueryClient,\n) => UseMutationResult<Response[\"data\"], Response[\"error\"], Init, TOnMutateResult>;\n\nexport interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {\n queryOptions: QueryOptionsFunction<Paths, Media>;\n useQuery: UseQueryMethod<Paths, Media>;\n useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;\n useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;\n useMutation: UseMutationMethod<Paths, Media>;\n}\n\nexport type MethodResponse<\n CreatedClient extends OpenapiQueryClient<any, any>,\n Method extends HttpMethod,\n Path extends CreatedClient extends OpenapiQueryClient<infer Paths, infer _Media>\n ? PathsWithMethod<Paths, Method>\n : never,\n Options = object,\n> =\n CreatedClient extends OpenapiQueryClient<infer Paths extends { [key: string]: any }, infer Media extends MediaType>\n ? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>[\"data\"]>\n : never;\n\n/**\n * Throw the error of a failed fetch result. `if (error)` missed two failures: a non-OK response with\n * an empty body (`error` is `undefined`, upstream #2070, #2829) and a falsy error body such as `0`.\n */\nfunction throwIfFailed(result: { error?: unknown; response: Response }): void {\n if (result.error !== undefined || !result.response.ok) {\n throw result.error;\n }\n}\n\n/** The query parameter that carries the page param unless `pageParamName` names another. */\nconst DEFAULT_PAGE_PARAM_NAME = \"cursor\";\n\ninterface PageRequest {\n readonly pageParamName: string;\n readonly pageParam: unknown;\n readonly signal: AbortSignal;\n}\n\n/**\n * The request init of one page. An `undefined` page param (the first page with\n * `initialPageParam: undefined`) is left out instead of being sent as `0` (upstream #2715, #2340,\n * ADR-022), so a cursor API gets no cursor on the first page.\n */\nfunction pageInit(init: unknown, { pageParamName, pageParam, signal }: PageRequest) {\n const { params, ...rest } = (init ?? {}) as { params?: { query?: DefaultParamsOption } };\n const page = pageParam === undefined ? {} : { [pageParamName]: pageParam };\n return { ...rest, signal, params: { ...params, query: { ...params?.query, ...page } } };\n}\n\n// TODO: Add the ability to bring queryClient as argument\nexport default function createClient<Paths extends {}, Media extends MediaType = MediaType>(\n client: FetchClient<Paths, Media>,\n): OpenapiQueryClient<Paths, Media> {\n const queryFn = async <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>>({\n queryKey,\n signal,\n }: QueryFunctionContext<QueryKey<Paths, Method, Path>>) => {\n // While `Path` is generic, TypeScript cannot work out that the key is a tuple: the init is a\n // conditional type per operation (`MaybeOptionalInit`), and the key's type waits for it.\n const [method, path, init] = queryKey as readonly [Method, Path, unknown?];\n const mth = method.toUpperCase() as Uppercase<typeof method>;\n const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;\n const result = await fn(path, { signal, ...(init as any) }); // TODO: find a way to avoid as any\n throwIfFailed(result);\n // TanStack Query rejects `undefined` query data. The fetch client gives `undefined` for every\n // success without a body: a 204, `Content-Length: 0`, an empty chunked body, a HEAD request\n // (upstream #2234). A parsed body is never `undefined`, so this changes nothing else.\n return result.data ?? null;\n };\n\n const queryOptions: QueryOptionsFunction<Paths, Media> = (method, path, ...[init, options]) => ({\n queryKey: (init === undefined ? ([method, path] as const) : ([method, path, init] as const)) as DataTag<\n QueryKey<Paths, typeof method, typeof path>,\n any,\n any\n >,\n queryFn,\n ...options,\n });\n\n return {\n queryOptions,\n useQuery: (method, path, ...[init, options, queryClient]) =>\n useQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),\n useSuspenseQuery: (method, path, ...[init, options, queryClient]) =>\n useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),\n useInfiniteQuery: (method, path, init, options, queryClient) => {\n const { pageParamName = DEFAULT_PAGE_PARAM_NAME, placeholderData, ...restOptions } = options;\n const { queryKey } = queryOptions(method, path, init);\n const mth = method.toUpperCase() as Uppercase<typeof method>;\n const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;\n return useInfiniteQuery(\n {\n queryKey: [...queryKey, INFINITE_QUERY_KEY_MARKER] as const,\n // The request comes from the arguments; the key only identifies the cache entry.\n queryFn: async ({ pageParam, signal }) => {\n const result = await fn(path, pageInit(init, { pageParamName, pageParam, signal }) as any);\n throwIfFailed(result);\n // An empty page stays `undefined`, as in upstream: TanStack Query rejects only\n // `undefined` query data, and the infinite query data is the `{ pages }` object.\n return result.data;\n },\n ...restOptions,\n // Typed with an `unknown` page param for the caller (`InfinitePlaceholderData`). TanStack\n // passes the previous data of this query, with its own page params, and takes it back.\n placeholderData: placeholderData as any,\n },\n queryClient,\n );\n },\n useMutation: (method, path, options, queryClient) =>\n useMutation(\n {\n mutationKey: [method, path],\n mutationFn: async (init) => {\n const mth = method.toUpperCase() as Uppercase<typeof method>;\n const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;\n const result = await fn(path, init as InitWithUnknowns<typeof init>);\n throwIfFailed(result);\n // TanStack Query accepts `undefined` mutation data, so a success without a body stays\n // `undefined`, as in upstream; only query data needs `null` (upstream #2234). The cast\n // only bridges the generic types: `Response[\"data\"]` still has `undefined` for an\n // operation with a response without a body.\n const { data } = result;\n return data as Exclude<typeof data, undefined>;\n },\n ...options,\n },\n queryClient,\n ),\n };\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,91 @@
1
1
  {
2
2
  "name": "openapi-typesafe-react-query",
3
- "version": "0.0.0",
4
- "description": "Reserved for openapi-typesafe. Contains no code.",
5
- "license": "MIT"
6
- }
3
+ "description": "Fast, type-safe @tanstack/react-query client to work with your OpenAPI schema.",
4
+ "version": "1.0.0-rc.1",
5
+ "author": {
6
+ "name": "Fedor Chishchin",
7
+ "url": "https://github.com/fedorello"
8
+ },
9
+ "contributors": [
10
+ {
11
+ "name": "Martin Paucot",
12
+ "email": "contact@martin-paucot.fr"
13
+ }
14
+ ],
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "main": "./dist/index.mjs",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.mjs",
21
+ "require": "./dist/index.cjs",
22
+ "default": "./dist/index.mjs"
23
+ },
24
+ "./*": "./*"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src"
29
+ ],
30
+ "homepage": "https://github.com/openapi-typesafe/openapi-typesafe#readme",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/openapi-typesafe/openapi-typesafe.git",
34
+ "directory": "packages/openapi-typesafe-react-query"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/openapi-typesafe/openapi-typesafe/issues"
38
+ },
39
+ "keywords": [
40
+ "openapi",
41
+ "swagger",
42
+ "rest",
43
+ "api",
44
+ "oapi_3",
45
+ "oapi_3_1",
46
+ "typescript",
47
+ "fetch",
48
+ "react",
49
+ "react-query",
50
+ "tanstack"
51
+ ],
52
+ "dependencies": {
53
+ "openapi-typesafe-helpers": "^1.0.0-rc.1"
54
+ },
55
+ "devDependencies": {
56
+ "@tanstack/react-query": "5.105.1",
57
+ "@testing-library/react": "16.3.3",
58
+ "@types/react": "19.3.0",
59
+ "@types/react-dom": "19.3.0",
60
+ "@vitejs/plugin-react": "6.1.2",
61
+ "execa": "^10.2.1",
62
+ "jsdom": "30.1.3",
63
+ "msw": "3.0.3",
64
+ "openapi-typesafe": "^1.0.0-rc.1",
65
+ "openapi-typesafe-fetch": "^1.0.0-rc.1",
66
+ "react": "19.3.1",
67
+ "react-dom": "19.3.1",
68
+ "react-error-boundary": "6.1.6"
69
+ },
70
+ "peerDependencies": {
71
+ "@tanstack/react-query": "^5.80.0",
72
+ "openapi-typesafe-fetch": "^1.0.0-rc.1"
73
+ },
74
+ "engines": {
75
+ "node": ">=22"
76
+ },
77
+ "scripts": {
78
+ "build": "unbuild",
79
+ "dev": "tsc -p tsconfig.build.json --watch",
80
+ "format": "biome format . --write",
81
+ "lint": "pnpm run lint:js && pnpm run lint:ts",
82
+ "lint:js": "biome check .",
83
+ "lint:ts": "tsc --noEmit",
84
+ "generate-types": "openapi-typesafe test/fixtures/api.yaml -o test/fixtures/api.d.ts && openapi-typesafe test/fixtures/query.yaml -o test/fixtures/query.d.ts",
85
+ "test": "pnpm run generate-types && pnpm run test:js && pnpm run test:exports",
86
+ "test:js": "vitest run",
87
+ "test:exports": "pnpm run build && ../../scripts/check-exports.sh",
88
+ "version": "pnpm run prepare && pnpm run build",
89
+ "test:coverage": "vitest run --coverage"
90
+ }
91
+ }
package/src/index.ts ADDED
@@ -0,0 +1,389 @@
1
+ import {
2
+ type DataTag,
3
+ type InfiniteData,
4
+ type QueryClient,
5
+ type QueryFunctionContext,
6
+ type SkipToken,
7
+ type UseInfiniteQueryOptions,
8
+ type UseInfiniteQueryResult,
9
+ type UseMutationOptions,
10
+ type UseMutationResult,
11
+ type UseQueryOptions,
12
+ type UseQueryResult,
13
+ type UseSuspenseQueryOptions,
14
+ type UseSuspenseQueryResult,
15
+ useInfiniteQuery,
16
+ useMutation,
17
+ useQuery,
18
+ useSuspenseQuery,
19
+ } from "@tanstack/react-query";
20
+ import type {
21
+ ClientMethod,
22
+ DefaultParamsOption,
23
+ Client as FetchClient,
24
+ FetchResponse,
25
+ IsInitOptional,
26
+ MaybeOptionalInit,
27
+ PathOperation,
28
+ } from "openapi-typesafe-fetch";
29
+ import type { HttpMethod, MediaType, PathsWithMethod } from "openapi-typesafe-helpers";
30
+
31
+ // Helper type to dynamically infer the type from the `select` property
32
+ type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
33
+
34
+ /**
35
+ * Query data of a response type: a success without a body is `undefined` in the fetch client and
36
+ * `null` in the query, as `queryFn` resolves it (upstream #2234, #2464).
37
+ */
38
+ type QueryData<Data> = undefined extends Data ? Exclude<Data, undefined> | null : Data;
39
+
40
+ type InitWithUnknowns<Init> = Init & { [key: string]: unknown };
41
+
42
+ /**
43
+ * The arguments after the path: the init, required when the operation requires a parameter or a
44
+ * body, then `Rest`. `IsInitOptional` keeps the init optional in
45
+ * `Parameters<typeof $api.queryOptions>`, where it is the union of all inits (upstream #2178).
46
+ */
47
+ type InitArguments<Init, Rest extends unknown[]> =
48
+ IsInitOptional<Init> extends true ? [InitWithUnknowns<Init>?, ...Rest] : [InitWithUnknowns<Init>, ...Rest];
49
+
50
+ export type QueryKey<
51
+ Paths extends {},
52
+ Method extends HttpMethod,
53
+ Path extends PathsWithMethod<Paths, Method>,
54
+ Init = MaybeOptionalInit<Paths[Path], Method>,
55
+ > = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];
56
+
57
+ /**
58
+ * The last element of an infinite query's key. `useQuery` and `useInfiniteQuery` cache different
59
+ * data (`TData` and `InfiniteData<TData>`), so the same request must not share one key (upstream
60
+ * #2184, ADR-022). A prefix such as `[method, path]` still matches both. Not exported: a runtime
61
+ * named export next to the default export would change what `require()` returns.
62
+ */
63
+ const INFINITE_QUERY_KEY_MARKER = "infinite";
64
+
65
+ export type InfiniteQueryKey<
66
+ Paths extends {},
67
+ Method extends HttpMethod,
68
+ Path extends PathsWithMethod<Paths, Method>,
69
+ > = readonly [...QueryKey<Paths, Method, Path>, typeof INFINITE_QUERY_KEY_MARKER];
70
+
71
+ export type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <
72
+ Method extends HttpMethod,
73
+ Path extends PathsWithMethod<Paths, Method>,
74
+ Init extends MaybeOptionalInit<Paths[Path], Method>,
75
+ Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
76
+ Options extends Omit<
77
+ UseQueryOptions<
78
+ QueryData<Response["data"]>,
79
+ Response["error"],
80
+ InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
81
+ QueryKey<Paths, Method, Path>
82
+ >,
83
+ "queryKey" | "queryFn"
84
+ >,
85
+ >(
86
+ method: Method,
87
+ path: Path,
88
+ ...[init, options]: InitArguments<Init, [Options?]>
89
+ ) => NoInfer<
90
+ Omit<
91
+ UseQueryOptions<
92
+ QueryData<Response["data"]>,
93
+ Response["error"],
94
+ InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
95
+ QueryKey<Paths, Method, Path>
96
+ >,
97
+ "queryFn" | "queryKey"
98
+ > & {
99
+ queryKey: DataTag<QueryKey<Paths, Method, Path>, QueryData<Response["data"]>, Response["error"]>;
100
+ queryFn: Exclude<
101
+ UseQueryOptions<
102
+ QueryData<Response["data"]>,
103
+ Response["error"],
104
+ InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
105
+ QueryKey<Paths, Method, Path>
106
+ >["queryFn"],
107
+ SkipToken | undefined
108
+ >;
109
+ }
110
+ >;
111
+
112
+ export type UseQueryMethod<Paths extends {}, Media extends MediaType> = <
113
+ Method extends HttpMethod,
114
+ Path extends PathsWithMethod<Paths, Method>,
115
+ Init extends MaybeOptionalInit<Paths[Path], Method>,
116
+ Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
117
+ Options extends Omit<
118
+ UseQueryOptions<
119
+ QueryData<Response["data"]>,
120
+ Response["error"],
121
+ InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
122
+ QueryKey<Paths, Method, Path>
123
+ >,
124
+ "queryKey" | "queryFn"
125
+ >,
126
+ >(
127
+ method: Method,
128
+ url: Path,
129
+ ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>
130
+ ) => UseQueryResult<InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, Response["error"]>;
131
+
132
+ /**
133
+ * The type of the page params: the initial one and the ones `getNextPageParam` and
134
+ * `getPreviousPageParam` return (upstream #2458). It was `unknown`. It stays `unknown` when a
135
+ * return type is untyped, so code that compiled still compiles (roadmap §3): TypeScript cannot
136
+ * infer the return type of a callback that computes it from an unannotated `lastPageParam`,
137
+ * whose type depends on that return type, and gives `never` or `unknown`.
138
+ */
139
+ type PageParam<Initial, Next, Previous> =
140
+ IsUntyped<Next> extends true
141
+ ? unknown
142
+ : [Previous] extends [NoPreviousPageParam]
143
+ ? NullishToUnknown<Initial | NonNullable<Next>>
144
+ : IsUntyped<Previous> extends true
145
+ ? unknown
146
+ : NullishToUnknown<Initial | NonNullable<Next> | NonNullable<Previous>>;
147
+
148
+ /** The `Previous` type parameter when the options have no `getPreviousPageParam`. */
149
+ declare const NO_PREVIOUS_PAGE_PARAM: unique symbol;
150
+ type NoPreviousPageParam = typeof NO_PREVIOUS_PAGE_PARAM;
151
+
152
+ type IsUntyped<T> = [T] extends [never] ? true : unknown extends T ? true : false;
153
+
154
+ /** Only `undefined` or `null` names no type: a cursor API's `initialPageParam: undefined`. */
155
+ type NullishToUnknown<T> = [T] extends [undefined | null] ? unknown : T;
156
+
157
+ /** Read only for the return type; `never` arguments add no inference for TanStack's page param. */
158
+ type PageParamFunction<Result> = (...args: never) => Result;
159
+
160
+ /**
161
+ * Inference sites of the page param types. Intersected before `Options`, so TanStack's own
162
+ * inference in the implementation reads the full signatures of `Options`.
163
+ */
164
+ interface PageParamSources<Initial, Next, Previous> {
165
+ initialPageParam: Initial;
166
+ getNextPageParam: PageParamFunction<Next>;
167
+ getPreviousPageParam?: PageParamFunction<Previous>;
168
+ }
169
+
170
+ /**
171
+ * `placeholderData` of `useInfiniteQuery`, with an `unknown` page param as before: TypeScript types
172
+ * a `(previousData) => previousData` callback while it still infers the page param, so it got
173
+ * `unknown` there and failed against the typed page param.
174
+ */
175
+ type InfinitePlaceholderData<
176
+ Data,
177
+ Error,
178
+ Paths extends {},
179
+ Method extends HttpMethod,
180
+ Path extends PathsWithMethod<Paths, Method>,
181
+ > = UseInfiniteQueryOptions<
182
+ Data,
183
+ Error,
184
+ InfiniteData<Data, unknown>,
185
+ InfiniteQueryKey<Paths, Method, Path>,
186
+ unknown
187
+ >["placeholderData"];
188
+
189
+ export type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> = <
190
+ Method extends HttpMethod,
191
+ Path extends PathsWithMethod<Paths, Method>,
192
+ Init extends MaybeOptionalInit<Paths[Path], Method>,
193
+ Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>,
194
+ Options extends Omit<
195
+ UseInfiniteQueryOptions<
196
+ Response["data"],
197
+ Response["error"],
198
+ InferSelectReturnType<InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>, Options["select"]>,
199
+ InfiniteQueryKey<Paths, Method, Path>,
200
+ PageParam<Initial, Next, Previous>
201
+ >,
202
+ "queryKey" | "queryFn" | "placeholderData"
203
+ > & {
204
+ placeholderData?: InfinitePlaceholderData<Response["data"], Response["error"], Paths, Method, Path>;
205
+ pageParamName?: string;
206
+ },
207
+ Initial = unknown,
208
+ Next = never,
209
+ Previous = NoPreviousPageParam,
210
+ >(
211
+ method: Method,
212
+ url: Path,
213
+ init: InitWithUnknowns<Init>,
214
+ options: PageParamSources<Initial, Next, Previous> & Options,
215
+ queryClient?: QueryClient,
216
+ ) => UseInfiniteQueryResult<
217
+ InferSelectReturnType<InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>, Options["select"]>,
218
+ Response["error"]
219
+ >;
220
+
221
+ export type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> = <
222
+ Method extends HttpMethod,
223
+ Path extends PathsWithMethod<Paths, Method>,
224
+ Init extends MaybeOptionalInit<Paths[Path], Method>,
225
+ Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
226
+ Options extends Omit<
227
+ UseSuspenseQueryOptions<
228
+ QueryData<Response["data"]>,
229
+ Response["error"],
230
+ InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
231
+ QueryKey<Paths, Method, Path>
232
+ >,
233
+ "queryKey" | "queryFn"
234
+ >,
235
+ >(
236
+ method: Method,
237
+ url: Path,
238
+ ...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>
239
+ ) => UseSuspenseQueryResult<InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>, Response["error"]>;
240
+
241
+ export type UseMutationMethod<Paths extends {}, Media extends MediaType> = <
242
+ Method extends HttpMethod,
243
+ Path extends PathsWithMethod<Paths, Method>,
244
+ Init extends MaybeOptionalInit<Paths[Path], Method>,
245
+ Response extends Required<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>, // note: Required is used to avoid repeating NonNullable in UseQuery types
246
+ TOnMutateResult = unknown,
247
+ >(
248
+ method: Method,
249
+ url: Path,
250
+ options?: Omit<
251
+ UseMutationOptions<Response["data"], Response["error"], Init, TOnMutateResult>,
252
+ "mutationKey" | "mutationFn"
253
+ >,
254
+ queryClient?: QueryClient,
255
+ ) => UseMutationResult<Response["data"], Response["error"], Init, TOnMutateResult>;
256
+
257
+ export interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
258
+ queryOptions: QueryOptionsFunction<Paths, Media>;
259
+ useQuery: UseQueryMethod<Paths, Media>;
260
+ useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
261
+ useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;
262
+ useMutation: UseMutationMethod<Paths, Media>;
263
+ }
264
+
265
+ export type MethodResponse<
266
+ CreatedClient extends OpenapiQueryClient<any, any>,
267
+ Method extends HttpMethod,
268
+ Path extends CreatedClient extends OpenapiQueryClient<infer Paths, infer _Media>
269
+ ? PathsWithMethod<Paths, Method>
270
+ : never,
271
+ Options = object,
272
+ > =
273
+ CreatedClient extends OpenapiQueryClient<infer Paths extends { [key: string]: any }, infer Media extends MediaType>
274
+ ? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>["data"]>
275
+ : never;
276
+
277
+ /**
278
+ * Throw the error of a failed fetch result. `if (error)` missed two failures: a non-OK response with
279
+ * an empty body (`error` is `undefined`, upstream #2070, #2829) and a falsy error body such as `0`.
280
+ */
281
+ function throwIfFailed(result: { error?: unknown; response: Response }): void {
282
+ if (result.error !== undefined || !result.response.ok) {
283
+ throw result.error;
284
+ }
285
+ }
286
+
287
+ /** The query parameter that carries the page param unless `pageParamName` names another. */
288
+ const DEFAULT_PAGE_PARAM_NAME = "cursor";
289
+
290
+ interface PageRequest {
291
+ readonly pageParamName: string;
292
+ readonly pageParam: unknown;
293
+ readonly signal: AbortSignal;
294
+ }
295
+
296
+ /**
297
+ * The request init of one page. An `undefined` page param (the first page with
298
+ * `initialPageParam: undefined`) is left out instead of being sent as `0` (upstream #2715, #2340,
299
+ * ADR-022), so a cursor API gets no cursor on the first page.
300
+ */
301
+ function pageInit(init: unknown, { pageParamName, pageParam, signal }: PageRequest) {
302
+ const { params, ...rest } = (init ?? {}) as { params?: { query?: DefaultParamsOption } };
303
+ const page = pageParam === undefined ? {} : { [pageParamName]: pageParam };
304
+ return { ...rest, signal, params: { ...params, query: { ...params?.query, ...page } } };
305
+ }
306
+
307
+ // TODO: Add the ability to bring queryClient as argument
308
+ export default function createClient<Paths extends {}, Media extends MediaType = MediaType>(
309
+ client: FetchClient<Paths, Media>,
310
+ ): OpenapiQueryClient<Paths, Media> {
311
+ const queryFn = async <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>>({
312
+ queryKey,
313
+ signal,
314
+ }: QueryFunctionContext<QueryKey<Paths, Method, Path>>) => {
315
+ // While `Path` is generic, TypeScript cannot work out that the key is a tuple: the init is a
316
+ // conditional type per operation (`MaybeOptionalInit`), and the key's type waits for it.
317
+ const [method, path, init] = queryKey as readonly [Method, Path, unknown?];
318
+ const mth = method.toUpperCase() as Uppercase<typeof method>;
319
+ const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
320
+ const result = await fn(path, { signal, ...(init as any) }); // TODO: find a way to avoid as any
321
+ throwIfFailed(result);
322
+ // TanStack Query rejects `undefined` query data. The fetch client gives `undefined` for every
323
+ // success without a body: a 204, `Content-Length: 0`, an empty chunked body, a HEAD request
324
+ // (upstream #2234). A parsed body is never `undefined`, so this changes nothing else.
325
+ return result.data ?? null;
326
+ };
327
+
328
+ const queryOptions: QueryOptionsFunction<Paths, Media> = (method, path, ...[init, options]) => ({
329
+ queryKey: (init === undefined ? ([method, path] as const) : ([method, path, init] as const)) as DataTag<
330
+ QueryKey<Paths, typeof method, typeof path>,
331
+ any,
332
+ any
333
+ >,
334
+ queryFn,
335
+ ...options,
336
+ });
337
+
338
+ return {
339
+ queryOptions,
340
+ useQuery: (method, path, ...[init, options, queryClient]) =>
341
+ useQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),
342
+ useSuspenseQuery: (method, path, ...[init, options, queryClient]) =>
343
+ useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),
344
+ useInfiniteQuery: (method, path, init, options, queryClient) => {
345
+ const { pageParamName = DEFAULT_PAGE_PARAM_NAME, placeholderData, ...restOptions } = options;
346
+ const { queryKey } = queryOptions(method, path, init);
347
+ const mth = method.toUpperCase() as Uppercase<typeof method>;
348
+ const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
349
+ return useInfiniteQuery(
350
+ {
351
+ queryKey: [...queryKey, INFINITE_QUERY_KEY_MARKER] as const,
352
+ // The request comes from the arguments; the key only identifies the cache entry.
353
+ queryFn: async ({ pageParam, signal }) => {
354
+ const result = await fn(path, pageInit(init, { pageParamName, pageParam, signal }) as any);
355
+ throwIfFailed(result);
356
+ // An empty page stays `undefined`, as in upstream: TanStack Query rejects only
357
+ // `undefined` query data, and the infinite query data is the `{ pages }` object.
358
+ return result.data;
359
+ },
360
+ ...restOptions,
361
+ // Typed with an `unknown` page param for the caller (`InfinitePlaceholderData`). TanStack
362
+ // passes the previous data of this query, with its own page params, and takes it back.
363
+ placeholderData: placeholderData as any,
364
+ },
365
+ queryClient,
366
+ );
367
+ },
368
+ useMutation: (method, path, options, queryClient) =>
369
+ useMutation(
370
+ {
371
+ mutationKey: [method, path],
372
+ mutationFn: async (init) => {
373
+ const mth = method.toUpperCase() as Uppercase<typeof method>;
374
+ const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
375
+ const result = await fn(path, init as InitWithUnknowns<typeof init>);
376
+ throwIfFailed(result);
377
+ // TanStack Query accepts `undefined` mutation data, so a success without a body stays
378
+ // `undefined`, as in upstream; only query data needs `null` (upstream #2234). The cast
379
+ // only bridges the generic types: `Response["data"]` still has `undefined` for an
380
+ // operation with a response without a body.
381
+ const { data } = result;
382
+ return data as Exclude<typeof data, undefined>;
383
+ },
384
+ ...options,
385
+ },
386
+ queryClient,
387
+ ),
388
+ };
389
+ }