@kizunajs/tanstack-query 2.0.0-beta.0

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.MD ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sondre Ørland
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @kizunajs/tanstack-query
2
+
3
+ `@kizunajs/tanstack-query` provides `new KizunaTanstackQuery()`, which builds TanStack Query options from a Kizuna api. Query keys, caching, and invalidation come from the routes you already wrote.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ pnpm add @kizunajs/tanstack-query
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { useQuery } from '@tanstack/react-query';
15
+ import { KizunaTanstackQuery } from '@kizunajs/tanstack-query';
16
+ import { apiClient } from './api-client';
17
+
18
+ const api = new KizunaTanstackQuery(apiClient);
19
+
20
+ const { data } = useQuery(
21
+ api.users.getUser.queryOptions({
22
+ input: {
23
+ params: {
24
+ id: '1',
25
+ },
26
+ },
27
+ })
28
+ );
29
+ ```
30
+
31
+ ## Documentation
32
+
33
+ [TanStack Query client](https://kizunajs.com/docs/clients/tanstack-query)
package/dist/index.cjs ADDED
@@ -0,0 +1,186 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _tanstack_query_core = require("@tanstack/query-core");
3
+ let kizunajs = require("kizunajs");
4
+ let _kizunajs_fetch = require("@kizunajs/fetch");
5
+ //#region src/errors.ts
6
+ /**
7
+ * Thrown when a route returns a status the api does not declare. Declared
8
+ * statuses come back as `data` instead.
9
+ */
10
+ var UndeclaredResponseError = class extends Error {
11
+ status;
12
+ body;
13
+ headers;
14
+ constructor(routeKey, status, body, headers) {
15
+ super(`${routeKey} responded ${status}, which its api does not declare.`);
16
+ this.name = "UndeclaredResponseError";
17
+ this.status = status;
18
+ this.body = body;
19
+ this.headers = headers;
20
+ }
21
+ };
22
+ /**
23
+ * Narrows an `error` from a query or mutation to {@link UndeclaredResponseError}.
24
+ */
25
+ const isUndeclaredResponseError = (error) => error instanceof UndeclaredResponseError;
26
+ /**
27
+ * Thrown by `streamOptions` when a streamed route answers a declared status that
28
+ * does not stream, such as its `400`. The query has nothing to accumulate.
29
+ */
30
+ var NonStreamResponseError = class extends Error {
31
+ status;
32
+ body;
33
+ headers;
34
+ constructor(routeKey, status, body, headers) {
35
+ super(`${routeKey} responded ${status}, which is not its streamed status.`);
36
+ this.name = "NonStreamResponseError";
37
+ this.status = status;
38
+ this.body = body;
39
+ this.headers = headers;
40
+ }
41
+ };
42
+ const isNonStreamResponseError = (error) => error instanceof NonStreamResponseError;
43
+ //#endregion
44
+ //#region src/keys.ts
45
+ const withoutFetchOptions = (input) => {
46
+ if (typeof input !== "object" || input === null) return input;
47
+ const { fetchOptions: _fetchOptions, ...rest } = input;
48
+ return Object.keys(rest).length === 0 ? void 0 : rest;
49
+ };
50
+ const buildQueryKey = (segments, input, type) => {
51
+ const hashable = withoutFetchOptions(input);
52
+ return hashable === void 0 ? [segments, { type }] : [segments, {
53
+ input: hashable,
54
+ type
55
+ }];
56
+ };
57
+ const buildPathKey = (segments) => [segments];
58
+ //#endregion
59
+ //#region src/proxy.ts
60
+ const declaresStatus = (route, status) => {
61
+ if (Object.hasOwn(route.responses, String(status))) return true;
62
+ return status === 400 && ("body" in route || "query" in route);
63
+ };
64
+ const withSignal = (args, signal) => {
65
+ if (signal === void 0) return args;
66
+ const { fetchOptions, ...rest } = args ?? {};
67
+ return {
68
+ ...rest,
69
+ fetchOptions: {
70
+ ...fetchOptions,
71
+ signal: fetchOptions?.signal ?? signal
72
+ }
73
+ };
74
+ };
75
+ const runRoute = async (clientFn, route, routeKey, args, signal) => {
76
+ const result = await clientFn(withSignal(args, signal));
77
+ if (!declaresStatus(route, result.status)) throw new UndeclaredResponseError(routeKey, result.status, result.body, result.headers);
78
+ return result;
79
+ };
80
+ const isQueryMethod = (route) => route.method === "GET" || route.method === "HEAD";
81
+ const buildProcedure = (segments, route, clientFn) => {
82
+ const routeKey = segments.join(".");
83
+ const call = (args) => clientFn(args);
84
+ if ((0, kizunajs.routeStreams)(route)) return {
85
+ streamOptions: (options) => {
86
+ const { input, refetchMode, ...rest } = options;
87
+ const skipped = input === _tanstack_query_core.skipToken;
88
+ return {
89
+ queryKey: buildQueryKey(segments, skipped ? void 0 : input, "stream"),
90
+ queryFn: skipped ? _tanstack_query_core.skipToken : (0, _tanstack_query_core.experimental_streamedQuery)({
91
+ streamFn: async ({ signal }) => {
92
+ const result = await runRoute(clientFn, route, routeKey, input, signal);
93
+ if (!(0, kizunajs.streamStatuses)(route).includes(result.status)) throw new NonStreamResponseError(routeKey, result.status, result.body, result.headers);
94
+ return result.body;
95
+ },
96
+ ...refetchMode === void 0 ? {} : { refetchMode }
97
+ }),
98
+ ...rest
99
+ };
100
+ },
101
+ streamKey: (options) => buildQueryKey(segments, options?.input, "stream"),
102
+ key: () => buildPathKey(segments),
103
+ call
104
+ };
105
+ if (!isQueryMethod(route)) return {
106
+ mutationOptions: (options) => ({
107
+ mutationKey: buildPathKey(segments),
108
+ mutationFn: (variables) => runRoute(clientFn, route, routeKey, variables, void 0),
109
+ ...options
110
+ }),
111
+ mutationKey: () => buildPathKey(segments),
112
+ key: () => buildPathKey(segments),
113
+ call
114
+ };
115
+ return {
116
+ queryOptions: (options) => {
117
+ const { input, ...rest } = options;
118
+ const skipped = input === _tanstack_query_core.skipToken;
119
+ return {
120
+ queryKey: buildQueryKey(segments, skipped ? void 0 : input, "query"),
121
+ queryFn: skipped ? _tanstack_query_core.skipToken : ({ signal }) => runRoute(clientFn, route, routeKey, input, signal),
122
+ ...rest
123
+ };
124
+ },
125
+ infiniteOptions: (options) => {
126
+ const { input, ...rest } = options;
127
+ const skipped = input === _tanstack_query_core.skipToken;
128
+ return {
129
+ queryKey: buildQueryKey(segments, skipped ? void 0 : input, "infinite"),
130
+ queryFn: skipped ? _tanstack_query_core.skipToken : ({ pageParam, signal }) => runRoute(clientFn, route, routeKey, input(pageParam), signal),
131
+ ...rest
132
+ };
133
+ },
134
+ queryKey: (options) => buildQueryKey(segments, options?.input, "query"),
135
+ infiniteKey: (options) => buildQueryKey(segments, options?.input, "infinite"),
136
+ key: () => buildPathKey(segments),
137
+ call
138
+ };
139
+ };
140
+ /**
141
+ * Walks the client itself. Each method carries the route it answers, so the
142
+ * shape of the client is the shape of the proxy and nothing else is needed.
143
+ */
144
+ const buildNode = (segments, clientNode) => {
145
+ const node = { key: () => buildPathKey(segments) };
146
+ for (const routeName of Object.keys(clientNode)) {
147
+ const child = clientNode[routeName];
148
+ const childSegments = [...segments, routeName];
149
+ const route = (0, _kizunajs_fetch.routeOf)(child);
150
+ if (route) node[routeName] = buildProcedure(childSegments, route, child);
151
+ else if (child && typeof child === "object") node[routeName] = buildNode(childSegments, child);
152
+ }
153
+ return node;
154
+ };
155
+ function buildQueryProxy(client) {
156
+ return buildNode([], client);
157
+ }
158
+ /**
159
+ * Builds TanStack Query options from a fetch client. Every client method
160
+ * carries the route it answers, so the client is the only argument.
161
+ *
162
+ * ```ts
163
+ * import { useQuery } from '@tanstack/react-query';
164
+ * import { KizunaTanstackQuery } from '@kizunajs/tanstack-query';
165
+ * import { apiClient } from './api-client.js';
166
+ *
167
+ * const api = new KizunaTanstackQuery(apiClient);
168
+ *
169
+ * useQuery(
170
+ * api.users.listUsers.queryOptions({
171
+ * input: {
172
+ * query: {
173
+ * page: 1,
174
+ * },
175
+ * },
176
+ * })
177
+ * );
178
+ * ```
179
+ */
180
+ const KizunaTanstackQuery = buildQueryProxy;
181
+ //#endregion
182
+ exports.KizunaTanstackQuery = KizunaTanstackQuery;
183
+ exports.NonStreamResponseError = NonStreamResponseError;
184
+ exports.UndeclaredResponseError = UndeclaredResponseError;
185
+ exports.isNonStreamResponseError = isNonStreamResponseError;
186
+ exports.isUndeclaredResponseError = isUndeclaredResponseError;
@@ -0,0 +1,232 @@
1
+ import { DataTag, DefaultError, InfiniteData, InfiniteQueryObserverOptions, MutationObserverOptions, QueryFunction, QueryFunctionContext, QueryObserverOptions, SkipToken } from "@tanstack/query-core";
2
+ import { RouteDefinition, Routes, StreamResponseDefinition } from "kizunajs";
3
+ import { Client, ClientArgs, ClientMethod, ClientResponse } from "@kizunajs/fetch";
4
+
5
+ //#region src/types.d.ts
6
+ type KizunaQueryKeyType = 'query' | 'infinite' | 'stream';
7
+ /**
8
+ * `[segments, { input, type }]`, for example
9
+ * `[['users', 'getUser'], { input: { params: { id: '1' } }, type: 'query' }]`.
10
+ */
11
+ type KizunaQueryKey = readonly [readonly string[], {
12
+ readonly input?: unknown;
13
+ readonly type: KizunaQueryKeyType;
14
+ }];
15
+ /**
16
+ * `[segments]`, the prefix every operation under a route or group matches.
17
+ */
18
+ type KizunaPathKey = readonly [readonly string[]];
19
+ type HasOptionalArgs<Args> = {} extends Args ? true : false;
20
+ type CallFn<Args, Result> = HasOptionalArgs<Args> extends true ? (args?: Args) => Promise<Result> : (args: Args) => Promise<Result>;
21
+ type QueryExtras<TQueryFnData, TError> = Omit<QueryObserverOptions<TQueryFnData, TError, unknown, TQueryFnData, KizunaQueryKey>, 'queryKey' | 'queryFn'>;
22
+ type QueryInput<Args> = HasOptionalArgs<Args> extends true ? {
23
+ input?: Args | SkipToken;
24
+ } : {
25
+ input: Args | SkipToken;
26
+ };
27
+ type QueryOptionsOut<U, TQueryFnData, TError> = Omit<NoInfer<U>, 'input'> & {
28
+ queryKey: DataTag<KizunaQueryKey, TQueryFnData, TError>;
29
+ queryFn: U extends {
30
+ input: SkipToken;
31
+ } ? SkipToken : (context: QueryFunctionContext<KizunaQueryKey>) => Promise<TQueryFnData>;
32
+ };
33
+ type QueryOptionsFn<Args, Result> = <U extends QueryInput<Args> & QueryExtras<Result, DefaultError>>(options: U) => QueryOptionsOut<U, Result, DefaultError>;
34
+ type InfiniteExtras<TQueryFnData, TError, TPageParam> = Omit<InfiniteQueryObserverOptions<TQueryFnData, TError, unknown, KizunaQueryKey, TPageParam>, 'queryKey' | 'queryFn'>;
35
+ type InfiniteOptionsOut<U, TQueryFnData, TError, TPageParam, Skipped extends boolean> = Omit<NoInfer<U>, 'input'> & {
36
+ queryKey: DataTag<KizunaQueryKey, InfiniteData<TQueryFnData, TPageParam>, TError>;
37
+ queryFn: Skipped extends true ? SkipToken : (context: QueryFunctionContext<KizunaQueryKey, TPageParam>) => Promise<TQueryFnData>;
38
+ };
39
+ interface InfiniteOptionsFn<Args, Result> {
40
+ <TPageParam, U extends InfiniteExtras<Result, DefaultError, TPageParam>>(options: {
41
+ input: (pageParam: TPageParam) => Args;
42
+ } & U): InfiniteOptionsOut<U, Result, DefaultError, TPageParam, false>;
43
+ <TPageParam, U extends InfiniteExtras<Result, DefaultError, TPageParam>>(options: {
44
+ input: SkipToken;
45
+ } & U): InfiniteOptionsOut<U, Result, DefaultError, TPageParam, true>;
46
+ }
47
+ type KeyFn<Args> = (options?: {
48
+ input?: Args;
49
+ }) => KizunaQueryKey;
50
+ /**
51
+ * A route whose method is `GET` or `HEAD`.
52
+ */
53
+ interface QueryProcedure<Args, Result> {
54
+ /**
55
+ * Options for `useQuery`. `data` is the route's declared response union; an
56
+ * undeclared status throws.
57
+ */
58
+ queryOptions: QueryOptionsFn<Args, Result>;
59
+ /**
60
+ * Options for `useInfiniteQuery`. `input` is a function of the page parameter.
61
+ */
62
+ infiniteOptions: InfiniteOptionsFn<Args, Result>;
63
+ /**
64
+ * The query's full key.
65
+ */
66
+ queryKey: KeyFn<Args>;
67
+ /**
68
+ * The infinite query's full key.
69
+ */
70
+ infiniteKey: KeyFn<Args>;
71
+ /**
72
+ * The partial key matching every operation on this route.
73
+ */
74
+ key: () => KizunaPathKey;
75
+ /**
76
+ * Calls the route, bypassing the cache.
77
+ */
78
+ call: CallFn<Args, Result>;
79
+ }
80
+ type MutationVariables<Args> = HasOptionalArgs<Args> extends true ? void : Args;
81
+ type MutationExtras<Args, Result, TError> = Omit<MutationObserverOptions<Result, TError, MutationVariables<Args>>, 'mutationKey' | 'mutationFn'>;
82
+ type MutationOptionsFn<Args, Result> = <U extends MutationExtras<Args, Result, DefaultError> = MutationExtras<Args, Result, DefaultError>>(options?: U) => NoInfer<U> & {
83
+ mutationKey: KizunaPathKey;
84
+ mutationFn: (variables: MutationVariables<Args>) => Promise<Result>;
85
+ };
86
+ /**
87
+ * A route whose method is anything other than `GET` or `HEAD`.
88
+ */
89
+ interface MutationProcedure<Args, Result> {
90
+ /**
91
+ * Options for `useMutation`. `mutate` takes the route's call arguments, or
92
+ * nothing when every argument is optional.
93
+ */
94
+ mutationOptions: MutationOptionsFn<Args, Result>;
95
+ /**
96
+ * The mutation's full key.
97
+ */
98
+ mutationKey: () => KizunaPathKey;
99
+ /**
100
+ * The partial key matching this route.
101
+ */
102
+ key: () => KizunaPathKey;
103
+ /**
104
+ * Calls the route, outside a mutation.
105
+ */
106
+ call: CallFn<Args, Result>;
107
+ }
108
+ /**
109
+ * Carried by the root and every group, for invalidating a whole group at once.
110
+ */
111
+ interface PathProcedures {
112
+ key: () => KizunaPathKey;
113
+ }
114
+ /**
115
+ * The messages a streamed result yields, read off the `AsyncIterable` body the
116
+ * client hands back rather than off the route's declaration.
117
+ */
118
+ type StreamMessageOfResult<Result> = Result extends {
119
+ body: AsyncIterable<infer Message>;
120
+ } ? Message : never;
121
+ type StreamExtras<TData, TError> = Omit<QueryObserverOptions<TData, TError, unknown, TData, KizunaQueryKey>, 'queryKey' | 'queryFn'> & {
122
+ /**
123
+ * What a refetch does with the messages already held: `'reset'` clears them
124
+ * first, `'append'` adds to them, `'replace'` swaps them in once the stream ends.
125
+ *
126
+ * @default 'reset'
127
+ */
128
+ refetchMode?: 'append' | 'reset' | 'replace';
129
+ };
130
+ type StreamOptionsOut<U, TData, TError> = Omit<NoInfer<U>, 'input' | 'refetchMode'> & {
131
+ queryKey: DataTag<KizunaQueryKey, TData, TError>;
132
+ queryFn: U extends {
133
+ input: SkipToken;
134
+ } ? SkipToken : QueryFunction<TData, KizunaQueryKey>;
135
+ };
136
+ type StreamOptionsFn<Args, Result> = <U extends QueryInput<Args> & StreamExtras<StreamMessageOfResult<Result>[], DefaultError>>(options: U) => StreamOptionsOut<U, StreamMessageOfResult<Result>[], DefaultError>;
137
+ /**
138
+ * A route whose response streams. `data` is the list of messages received so
139
+ * far, growing as they arrive.
140
+ */
141
+ interface StreamProcedure<Args, Result> {
142
+ /**
143
+ * Options for `useQuery`, over TanStack's `streamedQuery`. A status other than
144
+ * the streamed one throws `NonStreamResponseError`.
145
+ */
146
+ streamOptions: StreamOptionsFn<Args, Result>;
147
+ /**
148
+ * The stream query's full key.
149
+ */
150
+ streamKey: KeyFn<Args>;
151
+ key: () => KizunaPathKey;
152
+ call: CallFn<Args, Result>;
153
+ }
154
+ type HasStream<R extends RouteDefinition> = { [Status in keyof R['responses']]: R['responses'][Status] extends StreamResponseDefinition ? true : never }[keyof R['responses']] extends never ? false : true;
155
+ /**
156
+ * Which factories a route gets: a streamed response takes `streamOptions`, a
157
+ * `GET` or `HEAD` takes the query factories, and everything else mutates.
158
+ */
159
+ type ProcedureFor<Method, Streams, Args, Result> = Streams extends true ? StreamProcedure<Args, Result> : Method extends 'GET' | 'HEAD' ? QueryProcedure<Args, Result> : MutationProcedure<Args, Result>;
160
+ type Procedure<R extends RouteDefinition, Codes extends string> = ProcedureFor<R['method'], HasStream<R>, ClientArgs<R>, ClientResponse<R, Codes>>;
161
+ /**
162
+ * The route tree, each route carrying its query or mutation factories.
163
+ */
164
+ type KizunaQueryProxy<T extends Routes, Codes extends string = never> = { [K in keyof T as K extends string ? K : never]: T[K] extends RouteDefinition ? Procedure<T[K], Codes> : T[K] extends Routes ? KizunaQueryProxy<T[K], Codes> & PathProcedures : never };
165
+ /**
166
+ * The same tree, read off a generated client instead of off the routes. Each of
167
+ * its methods carries the method it calls and whether that response streams, so
168
+ * the client alone says which factories every route gets.
169
+ */
170
+ type GeneratedQueryProxy<C> = { [K in keyof C as K extends string ? K : never]: C[K] extends ClientMethod<infer Method, infer Streams, infer Args, infer Result> ? ProcedureFor<Method, Streams, Args, Result> : C[K] extends object ? GeneratedQueryProxy<C[K]> & PathProcedures : never };
171
+ interface KizunaTanstackQueryConstructor {
172
+ /**
173
+ * The client is the only argument: each of its methods carries the route it
174
+ * answers, so nothing has to hand over the api a second time.
175
+ */
176
+ new <T extends Routes, Codes extends string = never>(client: Client<T, Codes>): KizunaQueryProxy<T, Codes> & PathProcedures;
177
+ new <C extends object>(client: C): GeneratedQueryProxy<C> & PathProcedures;
178
+ }
179
+ //#endregion
180
+ //#region src/proxy.d.ts
181
+ /**
182
+ * Builds TanStack Query options from a fetch client. Every client method
183
+ * carries the route it answers, so the client is the only argument.
184
+ *
185
+ * ```ts
186
+ * import { useQuery } from '@tanstack/react-query';
187
+ * import { KizunaTanstackQuery } from '@kizunajs/tanstack-query';
188
+ * import { apiClient } from './api-client.js';
189
+ *
190
+ * const api = new KizunaTanstackQuery(apiClient);
191
+ *
192
+ * useQuery(
193
+ * api.users.listUsers.queryOptions({
194
+ * input: {
195
+ * query: {
196
+ * page: 1,
197
+ * },
198
+ * },
199
+ * })
200
+ * );
201
+ * ```
202
+ */
203
+ declare const KizunaTanstackQuery: KizunaTanstackQueryConstructor;
204
+ //#endregion
205
+ //#region src/errors.d.ts
206
+ /**
207
+ * Thrown when a route returns a status the api does not declare. Declared
208
+ * statuses come back as `data` instead.
209
+ */
210
+ declare class UndeclaredResponseError extends Error {
211
+ readonly status: number;
212
+ readonly body: unknown;
213
+ readonly headers: Record<string, string>;
214
+ constructor(routeKey: string, status: number, body: unknown, headers: Record<string, string>);
215
+ }
216
+ /**
217
+ * Narrows an `error` from a query or mutation to {@link UndeclaredResponseError}.
218
+ */
219
+ declare const isUndeclaredResponseError: (error: unknown) => error is UndeclaredResponseError;
220
+ /**
221
+ * Thrown by `streamOptions` when a streamed route answers a declared status that
222
+ * does not stream, such as its `400`. The query has nothing to accumulate.
223
+ */
224
+ declare class NonStreamResponseError extends Error {
225
+ readonly status: number;
226
+ readonly body: unknown;
227
+ readonly headers: Record<string, string>;
228
+ constructor(routeKey: string, status: number, body: unknown, headers: Record<string, string>);
229
+ }
230
+ declare const isNonStreamResponseError: (error: unknown) => error is NonStreamResponseError;
231
+ //#endregion
232
+ export { type KizunaPathKey, type KizunaQueryKey, type KizunaQueryKeyType, type KizunaQueryProxy, KizunaTanstackQuery, type KizunaTanstackQueryConstructor, type MutationProcedure, NonStreamResponseError, type PathProcedures, type QueryProcedure, type StreamProcedure, UndeclaredResponseError, isNonStreamResponseError, isUndeclaredResponseError };
@@ -0,0 +1,232 @@
1
+ import { DataTag, DefaultError, InfiniteData, InfiniteQueryObserverOptions, MutationObserverOptions, QueryFunction, QueryFunctionContext, QueryObserverOptions, SkipToken } from "@tanstack/query-core";
2
+ import { RouteDefinition, Routes, StreamResponseDefinition } from "kizunajs";
3
+ import { Client, ClientArgs, ClientMethod, ClientResponse } from "@kizunajs/fetch";
4
+
5
+ //#region src/types.d.ts
6
+ type KizunaQueryKeyType = 'query' | 'infinite' | 'stream';
7
+ /**
8
+ * `[segments, { input, type }]`, for example
9
+ * `[['users', 'getUser'], { input: { params: { id: '1' } }, type: 'query' }]`.
10
+ */
11
+ type KizunaQueryKey = readonly [readonly string[], {
12
+ readonly input?: unknown;
13
+ readonly type: KizunaQueryKeyType;
14
+ }];
15
+ /**
16
+ * `[segments]`, the prefix every operation under a route or group matches.
17
+ */
18
+ type KizunaPathKey = readonly [readonly string[]];
19
+ type HasOptionalArgs<Args> = {} extends Args ? true : false;
20
+ type CallFn<Args, Result> = HasOptionalArgs<Args> extends true ? (args?: Args) => Promise<Result> : (args: Args) => Promise<Result>;
21
+ type QueryExtras<TQueryFnData, TError> = Omit<QueryObserverOptions<TQueryFnData, TError, unknown, TQueryFnData, KizunaQueryKey>, 'queryKey' | 'queryFn'>;
22
+ type QueryInput<Args> = HasOptionalArgs<Args> extends true ? {
23
+ input?: Args | SkipToken;
24
+ } : {
25
+ input: Args | SkipToken;
26
+ };
27
+ type QueryOptionsOut<U, TQueryFnData, TError> = Omit<NoInfer<U>, 'input'> & {
28
+ queryKey: DataTag<KizunaQueryKey, TQueryFnData, TError>;
29
+ queryFn: U extends {
30
+ input: SkipToken;
31
+ } ? SkipToken : (context: QueryFunctionContext<KizunaQueryKey>) => Promise<TQueryFnData>;
32
+ };
33
+ type QueryOptionsFn<Args, Result> = <U extends QueryInput<Args> & QueryExtras<Result, DefaultError>>(options: U) => QueryOptionsOut<U, Result, DefaultError>;
34
+ type InfiniteExtras<TQueryFnData, TError, TPageParam> = Omit<InfiniteQueryObserverOptions<TQueryFnData, TError, unknown, KizunaQueryKey, TPageParam>, 'queryKey' | 'queryFn'>;
35
+ type InfiniteOptionsOut<U, TQueryFnData, TError, TPageParam, Skipped extends boolean> = Omit<NoInfer<U>, 'input'> & {
36
+ queryKey: DataTag<KizunaQueryKey, InfiniteData<TQueryFnData, TPageParam>, TError>;
37
+ queryFn: Skipped extends true ? SkipToken : (context: QueryFunctionContext<KizunaQueryKey, TPageParam>) => Promise<TQueryFnData>;
38
+ };
39
+ interface InfiniteOptionsFn<Args, Result> {
40
+ <TPageParam, U extends InfiniteExtras<Result, DefaultError, TPageParam>>(options: {
41
+ input: (pageParam: TPageParam) => Args;
42
+ } & U): InfiniteOptionsOut<U, Result, DefaultError, TPageParam, false>;
43
+ <TPageParam, U extends InfiniteExtras<Result, DefaultError, TPageParam>>(options: {
44
+ input: SkipToken;
45
+ } & U): InfiniteOptionsOut<U, Result, DefaultError, TPageParam, true>;
46
+ }
47
+ type KeyFn<Args> = (options?: {
48
+ input?: Args;
49
+ }) => KizunaQueryKey;
50
+ /**
51
+ * A route whose method is `GET` or `HEAD`.
52
+ */
53
+ interface QueryProcedure<Args, Result> {
54
+ /**
55
+ * Options for `useQuery`. `data` is the route's declared response union; an
56
+ * undeclared status throws.
57
+ */
58
+ queryOptions: QueryOptionsFn<Args, Result>;
59
+ /**
60
+ * Options for `useInfiniteQuery`. `input` is a function of the page parameter.
61
+ */
62
+ infiniteOptions: InfiniteOptionsFn<Args, Result>;
63
+ /**
64
+ * The query's full key.
65
+ */
66
+ queryKey: KeyFn<Args>;
67
+ /**
68
+ * The infinite query's full key.
69
+ */
70
+ infiniteKey: KeyFn<Args>;
71
+ /**
72
+ * The partial key matching every operation on this route.
73
+ */
74
+ key: () => KizunaPathKey;
75
+ /**
76
+ * Calls the route, bypassing the cache.
77
+ */
78
+ call: CallFn<Args, Result>;
79
+ }
80
+ type MutationVariables<Args> = HasOptionalArgs<Args> extends true ? void : Args;
81
+ type MutationExtras<Args, Result, TError> = Omit<MutationObserverOptions<Result, TError, MutationVariables<Args>>, 'mutationKey' | 'mutationFn'>;
82
+ type MutationOptionsFn<Args, Result> = <U extends MutationExtras<Args, Result, DefaultError> = MutationExtras<Args, Result, DefaultError>>(options?: U) => NoInfer<U> & {
83
+ mutationKey: KizunaPathKey;
84
+ mutationFn: (variables: MutationVariables<Args>) => Promise<Result>;
85
+ };
86
+ /**
87
+ * A route whose method is anything other than `GET` or `HEAD`.
88
+ */
89
+ interface MutationProcedure<Args, Result> {
90
+ /**
91
+ * Options for `useMutation`. `mutate` takes the route's call arguments, or
92
+ * nothing when every argument is optional.
93
+ */
94
+ mutationOptions: MutationOptionsFn<Args, Result>;
95
+ /**
96
+ * The mutation's full key.
97
+ */
98
+ mutationKey: () => KizunaPathKey;
99
+ /**
100
+ * The partial key matching this route.
101
+ */
102
+ key: () => KizunaPathKey;
103
+ /**
104
+ * Calls the route, outside a mutation.
105
+ */
106
+ call: CallFn<Args, Result>;
107
+ }
108
+ /**
109
+ * Carried by the root and every group, for invalidating a whole group at once.
110
+ */
111
+ interface PathProcedures {
112
+ key: () => KizunaPathKey;
113
+ }
114
+ /**
115
+ * The messages a streamed result yields, read off the `AsyncIterable` body the
116
+ * client hands back rather than off the route's declaration.
117
+ */
118
+ type StreamMessageOfResult<Result> = Result extends {
119
+ body: AsyncIterable<infer Message>;
120
+ } ? Message : never;
121
+ type StreamExtras<TData, TError> = Omit<QueryObserverOptions<TData, TError, unknown, TData, KizunaQueryKey>, 'queryKey' | 'queryFn'> & {
122
+ /**
123
+ * What a refetch does with the messages already held: `'reset'` clears them
124
+ * first, `'append'` adds to them, `'replace'` swaps them in once the stream ends.
125
+ *
126
+ * @default 'reset'
127
+ */
128
+ refetchMode?: 'append' | 'reset' | 'replace';
129
+ };
130
+ type StreamOptionsOut<U, TData, TError> = Omit<NoInfer<U>, 'input' | 'refetchMode'> & {
131
+ queryKey: DataTag<KizunaQueryKey, TData, TError>;
132
+ queryFn: U extends {
133
+ input: SkipToken;
134
+ } ? SkipToken : QueryFunction<TData, KizunaQueryKey>;
135
+ };
136
+ type StreamOptionsFn<Args, Result> = <U extends QueryInput<Args> & StreamExtras<StreamMessageOfResult<Result>[], DefaultError>>(options: U) => StreamOptionsOut<U, StreamMessageOfResult<Result>[], DefaultError>;
137
+ /**
138
+ * A route whose response streams. `data` is the list of messages received so
139
+ * far, growing as they arrive.
140
+ */
141
+ interface StreamProcedure<Args, Result> {
142
+ /**
143
+ * Options for `useQuery`, over TanStack's `streamedQuery`. A status other than
144
+ * the streamed one throws `NonStreamResponseError`.
145
+ */
146
+ streamOptions: StreamOptionsFn<Args, Result>;
147
+ /**
148
+ * The stream query's full key.
149
+ */
150
+ streamKey: KeyFn<Args>;
151
+ key: () => KizunaPathKey;
152
+ call: CallFn<Args, Result>;
153
+ }
154
+ type HasStream<R extends RouteDefinition> = { [Status in keyof R['responses']]: R['responses'][Status] extends StreamResponseDefinition ? true : never }[keyof R['responses']] extends never ? false : true;
155
+ /**
156
+ * Which factories a route gets: a streamed response takes `streamOptions`, a
157
+ * `GET` or `HEAD` takes the query factories, and everything else mutates.
158
+ */
159
+ type ProcedureFor<Method, Streams, Args, Result> = Streams extends true ? StreamProcedure<Args, Result> : Method extends 'GET' | 'HEAD' ? QueryProcedure<Args, Result> : MutationProcedure<Args, Result>;
160
+ type Procedure<R extends RouteDefinition, Codes extends string> = ProcedureFor<R['method'], HasStream<R>, ClientArgs<R>, ClientResponse<R, Codes>>;
161
+ /**
162
+ * The route tree, each route carrying its query or mutation factories.
163
+ */
164
+ type KizunaQueryProxy<T extends Routes, Codes extends string = never> = { [K in keyof T as K extends string ? K : never]: T[K] extends RouteDefinition ? Procedure<T[K], Codes> : T[K] extends Routes ? KizunaQueryProxy<T[K], Codes> & PathProcedures : never };
165
+ /**
166
+ * The same tree, read off a generated client instead of off the routes. Each of
167
+ * its methods carries the method it calls and whether that response streams, so
168
+ * the client alone says which factories every route gets.
169
+ */
170
+ type GeneratedQueryProxy<C> = { [K in keyof C as K extends string ? K : never]: C[K] extends ClientMethod<infer Method, infer Streams, infer Args, infer Result> ? ProcedureFor<Method, Streams, Args, Result> : C[K] extends object ? GeneratedQueryProxy<C[K]> & PathProcedures : never };
171
+ interface KizunaTanstackQueryConstructor {
172
+ /**
173
+ * The client is the only argument: each of its methods carries the route it
174
+ * answers, so nothing has to hand over the api a second time.
175
+ */
176
+ new <T extends Routes, Codes extends string = never>(client: Client<T, Codes>): KizunaQueryProxy<T, Codes> & PathProcedures;
177
+ new <C extends object>(client: C): GeneratedQueryProxy<C> & PathProcedures;
178
+ }
179
+ //#endregion
180
+ //#region src/proxy.d.ts
181
+ /**
182
+ * Builds TanStack Query options from a fetch client. Every client method
183
+ * carries the route it answers, so the client is the only argument.
184
+ *
185
+ * ```ts
186
+ * import { useQuery } from '@tanstack/react-query';
187
+ * import { KizunaTanstackQuery } from '@kizunajs/tanstack-query';
188
+ * import { apiClient } from './api-client.js';
189
+ *
190
+ * const api = new KizunaTanstackQuery(apiClient);
191
+ *
192
+ * useQuery(
193
+ * api.users.listUsers.queryOptions({
194
+ * input: {
195
+ * query: {
196
+ * page: 1,
197
+ * },
198
+ * },
199
+ * })
200
+ * );
201
+ * ```
202
+ */
203
+ declare const KizunaTanstackQuery: KizunaTanstackQueryConstructor;
204
+ //#endregion
205
+ //#region src/errors.d.ts
206
+ /**
207
+ * Thrown when a route returns a status the api does not declare. Declared
208
+ * statuses come back as `data` instead.
209
+ */
210
+ declare class UndeclaredResponseError extends Error {
211
+ readonly status: number;
212
+ readonly body: unknown;
213
+ readonly headers: Record<string, string>;
214
+ constructor(routeKey: string, status: number, body: unknown, headers: Record<string, string>);
215
+ }
216
+ /**
217
+ * Narrows an `error` from a query or mutation to {@link UndeclaredResponseError}.
218
+ */
219
+ declare const isUndeclaredResponseError: (error: unknown) => error is UndeclaredResponseError;
220
+ /**
221
+ * Thrown by `streamOptions` when a streamed route answers a declared status that
222
+ * does not stream, such as its `400`. The query has nothing to accumulate.
223
+ */
224
+ declare class NonStreamResponseError extends Error {
225
+ readonly status: number;
226
+ readonly body: unknown;
227
+ readonly headers: Record<string, string>;
228
+ constructor(routeKey: string, status: number, body: unknown, headers: Record<string, string>);
229
+ }
230
+ declare const isNonStreamResponseError: (error: unknown) => error is NonStreamResponseError;
231
+ //#endregion
232
+ export { type KizunaPathKey, type KizunaQueryKey, type KizunaQueryKeyType, type KizunaQueryProxy, KizunaTanstackQuery, type KizunaTanstackQueryConstructor, type MutationProcedure, NonStreamResponseError, type PathProcedures, type QueryProcedure, type StreamProcedure, UndeclaredResponseError, isNonStreamResponseError, isUndeclaredResponseError };
package/dist/index.mjs ADDED
@@ -0,0 +1,181 @@
1
+ import { experimental_streamedQuery, skipToken } from "@tanstack/query-core";
2
+ import { routeStreams, streamStatuses } from "kizunajs";
3
+ import { routeOf } from "@kizunajs/fetch";
4
+ //#region src/errors.ts
5
+ /**
6
+ * Thrown when a route returns a status the api does not declare. Declared
7
+ * statuses come back as `data` instead.
8
+ */
9
+ var UndeclaredResponseError = class extends Error {
10
+ status;
11
+ body;
12
+ headers;
13
+ constructor(routeKey, status, body, headers) {
14
+ super(`${routeKey} responded ${status}, which its api does not declare.`);
15
+ this.name = "UndeclaredResponseError";
16
+ this.status = status;
17
+ this.body = body;
18
+ this.headers = headers;
19
+ }
20
+ };
21
+ /**
22
+ * Narrows an `error` from a query or mutation to {@link UndeclaredResponseError}.
23
+ */
24
+ const isUndeclaredResponseError = (error) => error instanceof UndeclaredResponseError;
25
+ /**
26
+ * Thrown by `streamOptions` when a streamed route answers a declared status that
27
+ * does not stream, such as its `400`. The query has nothing to accumulate.
28
+ */
29
+ var NonStreamResponseError = class extends Error {
30
+ status;
31
+ body;
32
+ headers;
33
+ constructor(routeKey, status, body, headers) {
34
+ super(`${routeKey} responded ${status}, which is not its streamed status.`);
35
+ this.name = "NonStreamResponseError";
36
+ this.status = status;
37
+ this.body = body;
38
+ this.headers = headers;
39
+ }
40
+ };
41
+ const isNonStreamResponseError = (error) => error instanceof NonStreamResponseError;
42
+ //#endregion
43
+ //#region src/keys.ts
44
+ const withoutFetchOptions = (input) => {
45
+ if (typeof input !== "object" || input === null) return input;
46
+ const { fetchOptions: _fetchOptions, ...rest } = input;
47
+ return Object.keys(rest).length === 0 ? void 0 : rest;
48
+ };
49
+ const buildQueryKey = (segments, input, type) => {
50
+ const hashable = withoutFetchOptions(input);
51
+ return hashable === void 0 ? [segments, { type }] : [segments, {
52
+ input: hashable,
53
+ type
54
+ }];
55
+ };
56
+ const buildPathKey = (segments) => [segments];
57
+ //#endregion
58
+ //#region src/proxy.ts
59
+ const declaresStatus = (route, status) => {
60
+ if (Object.hasOwn(route.responses, String(status))) return true;
61
+ return status === 400 && ("body" in route || "query" in route);
62
+ };
63
+ const withSignal = (args, signal) => {
64
+ if (signal === void 0) return args;
65
+ const { fetchOptions, ...rest } = args ?? {};
66
+ return {
67
+ ...rest,
68
+ fetchOptions: {
69
+ ...fetchOptions,
70
+ signal: fetchOptions?.signal ?? signal
71
+ }
72
+ };
73
+ };
74
+ const runRoute = async (clientFn, route, routeKey, args, signal) => {
75
+ const result = await clientFn(withSignal(args, signal));
76
+ if (!declaresStatus(route, result.status)) throw new UndeclaredResponseError(routeKey, result.status, result.body, result.headers);
77
+ return result;
78
+ };
79
+ const isQueryMethod = (route) => route.method === "GET" || route.method === "HEAD";
80
+ const buildProcedure = (segments, route, clientFn) => {
81
+ const routeKey = segments.join(".");
82
+ const call = (args) => clientFn(args);
83
+ if (routeStreams(route)) return {
84
+ streamOptions: (options) => {
85
+ const { input, refetchMode, ...rest } = options;
86
+ const skipped = input === skipToken;
87
+ return {
88
+ queryKey: buildQueryKey(segments, skipped ? void 0 : input, "stream"),
89
+ queryFn: skipped ? skipToken : experimental_streamedQuery({
90
+ streamFn: async ({ signal }) => {
91
+ const result = await runRoute(clientFn, route, routeKey, input, signal);
92
+ if (!streamStatuses(route).includes(result.status)) throw new NonStreamResponseError(routeKey, result.status, result.body, result.headers);
93
+ return result.body;
94
+ },
95
+ ...refetchMode === void 0 ? {} : { refetchMode }
96
+ }),
97
+ ...rest
98
+ };
99
+ },
100
+ streamKey: (options) => buildQueryKey(segments, options?.input, "stream"),
101
+ key: () => buildPathKey(segments),
102
+ call
103
+ };
104
+ if (!isQueryMethod(route)) return {
105
+ mutationOptions: (options) => ({
106
+ mutationKey: buildPathKey(segments),
107
+ mutationFn: (variables) => runRoute(clientFn, route, routeKey, variables, void 0),
108
+ ...options
109
+ }),
110
+ mutationKey: () => buildPathKey(segments),
111
+ key: () => buildPathKey(segments),
112
+ call
113
+ };
114
+ return {
115
+ queryOptions: (options) => {
116
+ const { input, ...rest } = options;
117
+ const skipped = input === skipToken;
118
+ return {
119
+ queryKey: buildQueryKey(segments, skipped ? void 0 : input, "query"),
120
+ queryFn: skipped ? skipToken : ({ signal }) => runRoute(clientFn, route, routeKey, input, signal),
121
+ ...rest
122
+ };
123
+ },
124
+ infiniteOptions: (options) => {
125
+ const { input, ...rest } = options;
126
+ const skipped = input === skipToken;
127
+ return {
128
+ queryKey: buildQueryKey(segments, skipped ? void 0 : input, "infinite"),
129
+ queryFn: skipped ? skipToken : ({ pageParam, signal }) => runRoute(clientFn, route, routeKey, input(pageParam), signal),
130
+ ...rest
131
+ };
132
+ },
133
+ queryKey: (options) => buildQueryKey(segments, options?.input, "query"),
134
+ infiniteKey: (options) => buildQueryKey(segments, options?.input, "infinite"),
135
+ key: () => buildPathKey(segments),
136
+ call
137
+ };
138
+ };
139
+ /**
140
+ * Walks the client itself. Each method carries the route it answers, so the
141
+ * shape of the client is the shape of the proxy and nothing else is needed.
142
+ */
143
+ const buildNode = (segments, clientNode) => {
144
+ const node = { key: () => buildPathKey(segments) };
145
+ for (const routeName of Object.keys(clientNode)) {
146
+ const child = clientNode[routeName];
147
+ const childSegments = [...segments, routeName];
148
+ const route = routeOf(child);
149
+ if (route) node[routeName] = buildProcedure(childSegments, route, child);
150
+ else if (child && typeof child === "object") node[routeName] = buildNode(childSegments, child);
151
+ }
152
+ return node;
153
+ };
154
+ function buildQueryProxy(client) {
155
+ return buildNode([], client);
156
+ }
157
+ /**
158
+ * Builds TanStack Query options from a fetch client. Every client method
159
+ * carries the route it answers, so the client is the only argument.
160
+ *
161
+ * ```ts
162
+ * import { useQuery } from '@tanstack/react-query';
163
+ * import { KizunaTanstackQuery } from '@kizunajs/tanstack-query';
164
+ * import { apiClient } from './api-client.js';
165
+ *
166
+ * const api = new KizunaTanstackQuery(apiClient);
167
+ *
168
+ * useQuery(
169
+ * api.users.listUsers.queryOptions({
170
+ * input: {
171
+ * query: {
172
+ * page: 1,
173
+ * },
174
+ * },
175
+ * })
176
+ * );
177
+ * ```
178
+ */
179
+ const KizunaTanstackQuery = buildQueryProxy;
180
+ //#endregion
181
+ export { KizunaTanstackQuery, NonStreamResponseError, UndeclaredResponseError, isNonStreamResponseError, isUndeclaredResponseError };
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@kizunajs/tanstack-query",
3
+ "version": "2.0.0-beta.0",
4
+ "description": "TanStack Query client for Kizuna.js",
5
+ "keywords": [
6
+ "kizunajs",
7
+ "typescript",
8
+ "zod",
9
+ "openapi",
10
+ "spec-driven",
11
+ "type-safe",
12
+ "rest",
13
+ "api",
14
+ "tanstack-query",
15
+ "react-query",
16
+ "query-options",
17
+ "typed-client"
18
+ ],
19
+ "license": "MIT",
20
+ "homepage": "https://kizunajs.com/docs/clients/tanstack-query",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/kizunajs/kizuna.git",
24
+ "directory": "packages/tanstack-query"
25
+ },
26
+ "bugs": "https://github.com/kizunajs/kizuna/issues",
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "engines": {
30
+ "node": ">=24"
31
+ },
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.mjs",
34
+ "types": "./dist/index.d.mts",
35
+ "exports": {
36
+ ".": {
37
+ "import": {
38
+ "types": "./dist/index.d.mts",
39
+ "default": "./dist/index.mjs"
40
+ },
41
+ "require": {
42
+ "types": "./dist/index.d.cts",
43
+ "default": "./dist/index.cjs"
44
+ }
45
+ }
46
+ },
47
+ "kizuna": {
48
+ "entries": {
49
+ ".": "client"
50
+ }
51
+ },
52
+ "files": [
53
+ "dist"
54
+ ],
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "peerDependencies": {
59
+ "@tanstack/query-core": "^5.69.0",
60
+ "zod": "^4.0.0",
61
+ "kizunajs": "2.0.0-beta.0",
62
+ "@kizunajs/fetch": "2.0.0-beta.0"
63
+ },
64
+ "devDependencies": {
65
+ "@tanstack/query-core": "^5.101.0",
66
+ "@tanstack/react-query": "^5.101.0",
67
+ "@tanstack/svelte-query": "^6.1.0",
68
+ "@tanstack/vue-query": "^5.101.0",
69
+ "@types/react": "^19.2.18",
70
+ "react": "^19.2.8",
71
+ "svelte": "^5.0.0",
72
+ "tsdown": "^0.21.0",
73
+ "typescript": "^6.0.3",
74
+ "vue": "^3.5.0",
75
+ "zod": "^4.0.0",
76
+ "kizunajs": "2.0.0-beta.0",
77
+ "@kizunajs/fetch": "2.0.0-beta.0"
78
+ },
79
+ "scripts": {
80
+ "build": "tsdown src/index.ts --format esm,cjs --dts --clean --external kizunajs --external @kizunajs/fetch --external @tanstack/query-core --external zod",
81
+ "typecheck": "tsc --noEmit"
82
+ }
83
+ }