@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 +9 -0
- package/README.md +33 -0
- package/dist/index.cjs +186 -0
- package/dist/index.d.cts +232 -0
- package/dist/index.d.mts +232 -0
- package/dist/index.mjs +181 -0
- package/package.json +83 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.mts
ADDED
|
@@ -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
|
+
}
|