openapi-typesafe-react-query 1.0.0-rc.2 → 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/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: a runtime
70
- * named export next to the default export would change what `require()` returns.
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
- queryKey: DataTag<
152
+ > &
153
+ QueryKeyWithDataTag<
110
154
  QueryKey<Paths, Method, Path>,
111
155
  QueryData<Response["data"]>,
112
- Response["error"]
113
- >;
114
- queryFn: Exclude<
115
- UseQueryOptions<
116
- QueryData<Response["data"]>,
117
- Response["error"],
118
- InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
119
- QueryKey<Paths, Method, Path>
120
- >["queryFn"],
121
- SkipToken | undefined
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"], Init, TOnMutateResult>,
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"], Init, TOnMutateResult>;
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 || !result.response.ok) {
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. */