openapi-typesafe-react-query 1.0.0-rc.3 → 1.0.0-rc.4
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/CHANGELOG.md +398 -0
- package/README.md +10 -0
- package/dist/index.cjs +24 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -14
- package/dist/index.d.mts +55 -15
- package/dist/index.d.ts +55 -15
- package/dist/index.mjs +20 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -9
- package/src/index.ts +106 -30
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type DataTag,
|
|
3
|
+
type DefaultError,
|
|
3
4
|
type InfiniteData,
|
|
4
5
|
type QueryClient,
|
|
5
6
|
type QueryFunctionContext,
|
|
@@ -25,12 +26,15 @@ import type {
|
|
|
25
26
|
MaybeOptionalInit,
|
|
26
27
|
PathOperation,
|
|
27
28
|
} from "openapi-typesafe-fetch";
|
|
29
|
+
// The helper types come from the client, which declares them: types inferred from the hooks then
|
|
30
|
+
// print the client's `helpers` entry, of a peer every user has, and not `openapi-typesafe-helpers`,
|
|
31
|
+
// which Yarn PnP does not let a library reach (ADR-034).
|
|
28
32
|
import type {
|
|
29
33
|
HttpMethod,
|
|
30
34
|
MediaType,
|
|
31
35
|
PathsWithMethod,
|
|
32
36
|
RequiredKeysOf,
|
|
33
|
-
} from "openapi-typesafe-helpers";
|
|
37
|
+
} from "openapi-typesafe-fetch/helpers";
|
|
34
38
|
|
|
35
39
|
// Helper type to dynamically infer the type from the `select` property
|
|
36
40
|
type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
|
|
@@ -41,6 +45,46 @@ type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => in
|
|
|
41
45
|
*/
|
|
42
46
|
type QueryData<Data> = undefined extends Data ? Exclude<Data, undefined> | null : Data;
|
|
43
47
|
|
|
48
|
+
/**
|
|
49
|
+
* What the hooks throw when a request fails and the response has no error body: a non-OK status
|
|
50
|
+
* with an empty body, a JSON `null` body, or one whose operation declares no content for it. The
|
|
51
|
+
* fetch client gives `error: undefined` (or `null`) then; throwing that left `error` empty in
|
|
52
|
+
* TanStack Query while its type said there was an error body. Tell it apart with
|
|
53
|
+
* `isResponseError(error)`: `error instanceof Error` also matches a network or parse error. The
|
|
54
|
+
* message names the status, not the URL, which can carry secrets.
|
|
55
|
+
*/
|
|
56
|
+
export interface ResponseError extends Error {
|
|
57
|
+
readonly name: "ResponseError";
|
|
58
|
+
/** The response that failed. */
|
|
59
|
+
readonly response: Response;
|
|
60
|
+
/** Its HTTP status, as `response.status`. */
|
|
61
|
+
readonly status: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The error of a hook: the error body of a failed response, a `ResponseError` when it has none, or
|
|
66
|
+
* what the request threw before there was a response to read: a network error (`TypeError`), an
|
|
67
|
+
* abort, a body that is not JSON (`SyntaxError`) or a middleware error. TanStack Query types that
|
|
68
|
+
* last part as `DefaultError` (`Error` unless `Register` says otherwise; a registered `unknown`
|
|
69
|
+
* makes the whole type `unknown`, as for TanStack's own hooks). `undefined` and `null` are left
|
|
70
|
+
* out: a failed response without an error body throws a `ResponseError`.
|
|
71
|
+
*/
|
|
72
|
+
export type QueryError<ErrorBody> =
|
|
73
|
+
| Exclude<ErrorBody, undefined | null>
|
|
74
|
+
| ResponseError
|
|
75
|
+
| DefaultError;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* An object whose `queryKey` is tagged with the query's data and error types: the type that
|
|
79
|
+
* TanStack Query 5.102 exports as `QueryKeyWithDataTag`, for the whole peer range. A named object
|
|
80
|
+
* type, so the declarations of an exported `queryOptions` result print it by name. Spelled out,
|
|
81
|
+
* the tag prints TanStack's `dataTagSymbol` keys, which a declaration file cannot import, and
|
|
82
|
+
* the library's users get TS2304, or `any` cached data with `skipLibCheck`.
|
|
83
|
+
*/
|
|
84
|
+
export type QueryKeyWithDataTag<Key, Data, Error> = {
|
|
85
|
+
queryKey: DataTag<Key, Data, Error>;
|
|
86
|
+
};
|
|
87
|
+
|
|
44
88
|
type InitWithUnknowns<Init> = Init & { [key: string]: unknown };
|
|
45
89
|
|
|
46
90
|
/**
|
|
@@ -66,8 +110,8 @@ export type QueryKey<
|
|
|
66
110
|
/**
|
|
67
111
|
* The last element of an infinite query's key. `useQuery` and `useInfiniteQuery` cache different
|
|
68
112
|
* data (`TData` and `InfiniteData<TData>`), so the same request must not share one key (upstream
|
|
69
|
-
* #2184, ADR-022). A prefix such as `[method, path]` still matches both. Not exported:
|
|
70
|
-
*
|
|
113
|
+
* #2184, ADR-022). A prefix such as `[method, path]` still matches both. Not exported: it is not
|
|
114
|
+
* needed outside the package, and a named export is public API; `InfiniteQueryKey` types the key.
|
|
71
115
|
*/
|
|
72
116
|
const INFINITE_QUERY_KEY_MARKER = "infinite";
|
|
73
117
|
|
|
@@ -86,7 +130,7 @@ export type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <
|
|
|
86
130
|
Options extends Omit<
|
|
87
131
|
UseQueryOptions<
|
|
88
132
|
QueryData<Response["data"]>,
|
|
89
|
-
Response["error"]
|
|
133
|
+
QueryError<Response["error"]>,
|
|
90
134
|
InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
|
|
91
135
|
QueryKey<Paths, Method, Path>
|
|
92
136
|
>,
|
|
@@ -100,27 +144,27 @@ export type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <
|
|
|
100
144
|
Omit<
|
|
101
145
|
UseQueryOptions<
|
|
102
146
|
QueryData<Response["data"]>,
|
|
103
|
-
Response["error"]
|
|
147
|
+
QueryError<Response["error"]>,
|
|
104
148
|
InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
|
|
105
149
|
QueryKey<Paths, Method, Path>
|
|
106
150
|
>,
|
|
107
151
|
"queryFn" | "queryKey"
|
|
108
|
-
> &
|
|
109
|
-
|
|
152
|
+
> &
|
|
153
|
+
QueryKeyWithDataTag<
|
|
110
154
|
QueryKey<Paths, Method, Path>,
|
|
111
155
|
QueryData<Response["data"]>,
|
|
112
|
-
Response["error"]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
QueryError<Response["error"]>
|
|
157
|
+
> & {
|
|
158
|
+
queryFn: Exclude<
|
|
159
|
+
UseQueryOptions<
|
|
160
|
+
QueryData<Response["data"]>,
|
|
161
|
+
QueryError<Response["error"]>,
|
|
162
|
+
InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
|
|
163
|
+
QueryKey<Paths, Method, Path>
|
|
164
|
+
>["queryFn"],
|
|
165
|
+
SkipToken | undefined
|
|
166
|
+
>;
|
|
167
|
+
}
|
|
124
168
|
>;
|
|
125
169
|
|
|
126
170
|
export type UseQueryMethod<Paths extends {}, Media extends MediaType> = <
|
|
@@ -132,7 +176,7 @@ export type UseQueryMethod<Paths extends {}, Media extends MediaType> = <
|
|
|
132
176
|
Options extends Omit<
|
|
133
177
|
UseQueryOptions<
|
|
134
178
|
QueryData<Response["data"]>,
|
|
135
|
-
Response["error"]
|
|
179
|
+
QueryError<Response["error"]>,
|
|
136
180
|
InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
|
|
137
181
|
QueryKey<Paths, Method, Path>
|
|
138
182
|
>,
|
|
@@ -144,7 +188,7 @@ export type UseQueryMethod<Paths extends {}, Media extends MediaType> = <
|
|
|
144
188
|
...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>
|
|
145
189
|
) => UseQueryResult<
|
|
146
190
|
InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
|
|
147
|
-
Response["error"]
|
|
191
|
+
QueryError<Response["error"]>
|
|
148
192
|
>;
|
|
149
193
|
|
|
150
194
|
/**
|
|
@@ -212,7 +256,7 @@ export type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> =
|
|
|
212
256
|
Options extends Omit<
|
|
213
257
|
UseInfiniteQueryOptions<
|
|
214
258
|
Response["data"],
|
|
215
|
-
Response["error"]
|
|
259
|
+
QueryError<Response["error"]>,
|
|
216
260
|
InferSelectReturnType<
|
|
217
261
|
InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>,
|
|
218
262
|
Options["select"]
|
|
@@ -224,7 +268,7 @@ export type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> =
|
|
|
224
268
|
> & {
|
|
225
269
|
placeholderData?: InfinitePlaceholderData<
|
|
226
270
|
Response["data"],
|
|
227
|
-
Response["error"]
|
|
271
|
+
QueryError<Response["error"]>,
|
|
228
272
|
Paths,
|
|
229
273
|
Method,
|
|
230
274
|
Path
|
|
@@ -246,7 +290,7 @@ export type UseInfiniteQueryMethod<Paths extends {}, Media extends MediaType> =
|
|
|
246
290
|
InfiniteData<Response["data"], PageParam<Initial, Next, Previous>>,
|
|
247
291
|
Options["select"]
|
|
248
292
|
>,
|
|
249
|
-
Response["error"]
|
|
293
|
+
QueryError<Response["error"]>
|
|
250
294
|
>;
|
|
251
295
|
|
|
252
296
|
export type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> = <
|
|
@@ -258,7 +302,7 @@ export type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> =
|
|
|
258
302
|
Options extends Omit<
|
|
259
303
|
UseSuspenseQueryOptions<
|
|
260
304
|
QueryData<Response["data"]>,
|
|
261
|
-
Response["error"]
|
|
305
|
+
QueryError<Response["error"]>,
|
|
262
306
|
InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
|
|
263
307
|
QueryKey<Paths, Method, Path>
|
|
264
308
|
>,
|
|
@@ -270,7 +314,7 @@ export type UseSuspenseQueryMethod<Paths extends {}, Media extends MediaType> =
|
|
|
270
314
|
...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>
|
|
271
315
|
) => UseSuspenseQueryResult<
|
|
272
316
|
InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
|
|
273
|
-
Response["error"]
|
|
317
|
+
QueryError<Response["error"]>
|
|
274
318
|
>;
|
|
275
319
|
|
|
276
320
|
export type UseMutationMethod<Paths extends {}, Media extends MediaType> = <
|
|
@@ -284,11 +328,11 @@ export type UseMutationMethod<Paths extends {}, Media extends MediaType> = <
|
|
|
284
328
|
method: Method,
|
|
285
329
|
url: Path,
|
|
286
330
|
options?: Omit<
|
|
287
|
-
UseMutationOptions<Response["data"], Response["error"]
|
|
331
|
+
UseMutationOptions<Response["data"], QueryError<Response["error"]>, Init, TOnMutateResult>,
|
|
288
332
|
"mutationKey" | "mutationFn"
|
|
289
333
|
>,
|
|
290
334
|
queryClient?: QueryClient,
|
|
291
|
-
) => UseMutationResult<Response["data"], Response["error"]
|
|
335
|
+
) => UseMutationResult<Response["data"], QueryError<Response["error"]>, Init, TOnMutateResult>;
|
|
292
336
|
|
|
293
337
|
export interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
|
|
294
338
|
queryOptions: QueryOptionsFunction<Paths, Media>;
|
|
@@ -315,15 +359,47 @@ export type MethodResponse<
|
|
|
315
359
|
? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>["data"]>
|
|
316
360
|
: never;
|
|
317
361
|
|
|
362
|
+
/** The `name` of every `ResponseError`. */
|
|
363
|
+
const RESPONSE_ERROR_NAME = "ResponseError";
|
|
364
|
+
|
|
365
|
+
/** The `ResponseError` of a failed response without an error body. */
|
|
366
|
+
class ResponseWithoutErrorBody extends Error implements ResponseError {
|
|
367
|
+
override readonly name = RESPONSE_ERROR_NAME;
|
|
368
|
+
readonly response: Response;
|
|
369
|
+
readonly status: number;
|
|
370
|
+
|
|
371
|
+
constructor(response: Response) {
|
|
372
|
+
const statusText = response.statusText ? ` ${response.statusText}` : "";
|
|
373
|
+
super(`Request failed with status ${response.status}${statusText} and no error body`);
|
|
374
|
+
this.response = response;
|
|
375
|
+
this.status = response.status;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Whether `error` is the `ResponseError` of a failed response without an error body. It narrows a
|
|
381
|
+
* hook's `error`, where `error instanceof Error` also matches network and parse errors. It checks
|
|
382
|
+
* the `name`, not the class, so a `ResponseError` of another copy of this package (its CommonJS and
|
|
383
|
+
* ES module builds loaded side by side) is one too. An error body from JSON is never an `Error`.
|
|
384
|
+
*/
|
|
385
|
+
export function isResponseError(error: unknown): error is ResponseError {
|
|
386
|
+
return error instanceof Error && error.name === RESPONSE_ERROR_NAME;
|
|
387
|
+
}
|
|
388
|
+
|
|
318
389
|
/**
|
|
319
390
|
* Throw the error of a failed fetch result. `if (error)` missed two failures: a non-OK response
|
|
320
391
|
* with an empty body (`error` is `undefined`, upstream #2070, #2829) and a falsy error body such as
|
|
321
|
-
* `0`.
|
|
392
|
+
* `0`. Without an error body a `ResponseError` is thrown, so the hooks' `error` is never
|
|
393
|
+
* `undefined` (ADR-013, decision 3). A JSON `null` body is no error body either: thrown as is, it
|
|
394
|
+
* left `error` `null` while the query's status was `"error"`.
|
|
322
395
|
*/
|
|
323
396
|
function throwIfFailed(result: { error?: unknown; response: Response }): void {
|
|
324
|
-
if (result.error !== undefined
|
|
397
|
+
if (result.error !== undefined && result.error !== null) {
|
|
325
398
|
throw result.error;
|
|
326
399
|
}
|
|
400
|
+
if (!result.response.ok) {
|
|
401
|
+
throw new ResponseWithoutErrorBody(result.response);
|
|
402
|
+
}
|
|
327
403
|
}
|
|
328
404
|
|
|
329
405
|
/** The query parameter that carries the page param unless `pageParamName` names another. */
|