@r0hitsharma/http-client-react 0.12.0-rohit-fork-ci.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +378 -0
- package/README.md +427 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.js +51 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +29 -0
- package/dist/middleware.d.ts +45 -0
- package/dist/middleware.js +25 -0
- package/dist/query-api.d.ts +114 -0
- package/dist/query-api.js +128 -0
- package/dist/query-client.d.ts +84 -0
- package/dist/query-client.js +152 -0
- package/dist/query-key.d.ts +50 -0
- package/dist/query-key.js +70 -0
- package/dist/tags.d.ts +28 -0
- package/dist/tags.js +53 -0
- package/dist/zod-response.d.ts +61 -0
- package/dist/zod-response.js +91 -0
- package/oxfmt.config.ts +5 -0
- package/oxlint.config.ts +5 -0
- package/package.json +49 -0
- package/src/deprecated.test.ts +27 -0
- package/src/errors.ts +74 -0
- package/src/index.tsx +100 -0
- package/src/middleware.test.ts +88 -0
- package/src/middleware.ts +84 -0
- package/src/query-api.test.ts +644 -0
- package/src/query-api.ts +512 -0
- package/src/query-api.types.test.ts +225 -0
- package/src/query-client.test.ts +315 -0
- package/src/query-client.ts +173 -0
- package/src/query-key.test.ts +121 -0
- package/src/query-key.ts +113 -0
- package/src/tags.ts +100 -0
- package/src/test-fixtures.ts +222 -0
- package/src/zod-response.ts +150 -0
- package/tsconfig.build.json +21 -0
- package/tsconfig.json +7 -0
- package/vitest.config.ts +11 -0
package/src/query-api.ts
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiClient,
|
|
3
|
+
FetchResponse,
|
|
4
|
+
HttpMethod,
|
|
5
|
+
MaybeOptionalInit,
|
|
6
|
+
MediaType,
|
|
7
|
+
PathsWithMethod,
|
|
8
|
+
RequiredKeysOf,
|
|
9
|
+
} from '@r0hitsharma/http-client-core';
|
|
10
|
+
import type {
|
|
11
|
+
DataTag,
|
|
12
|
+
QueryClient,
|
|
13
|
+
QueryFilters,
|
|
14
|
+
QueryKey,
|
|
15
|
+
SkipToken,
|
|
16
|
+
UseMutationOptions,
|
|
17
|
+
UseQueryOptions,
|
|
18
|
+
} from '@tanstack/react-query';
|
|
19
|
+
|
|
20
|
+
import { HttpRequestError } from './errors.js';
|
|
21
|
+
import {
|
|
22
|
+
composeMiddleware,
|
|
23
|
+
type QueryApiMiddleware,
|
|
24
|
+
type QueryApiOperationType,
|
|
25
|
+
type QueryApiRequestContext,
|
|
26
|
+
} from './middleware.js';
|
|
27
|
+
import { buildQueryApiKey, type QueryApiKey } from './query-key.js';
|
|
28
|
+
import { createTagRegistry } from './tags.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The shape an `openapi-typescript`-generated `paths` type has: every path maps
|
|
32
|
+
* to an object carrying the HTTP methods that endpoint implements, plus the
|
|
33
|
+
* path-level `parameters`. This is the constraint on `createQueryApi` and on
|
|
34
|
+
* every option type below, and it is what makes `TPaths[TPath][TMethod]` legal.
|
|
35
|
+
*
|
|
36
|
+
* Every method is *optional* and typed `any` for two reasons that come straight
|
|
37
|
+
* from the generated output. `openapi-typescript` emits absent operations as
|
|
38
|
+
* `put?: never`, so a required key rejects real generated paths; and the
|
|
39
|
+
* operation payload has to widen to `any`, because narrowing it makes
|
|
40
|
+
* TypeScript resolve `TPaths[TPath][TMethod]` to `<payload> | undefined`, which
|
|
41
|
+
* then fails `FetchResponse`'s own `Record<string | number, any>` constraint.
|
|
42
|
+
* Nothing is lost by that: the operation types the api actually reports are
|
|
43
|
+
* read back off the caller's own `TPaths`, never off this shape.
|
|
44
|
+
*
|
|
45
|
+
* What it therefore does *not* do is reject a badly typed client. If the paths
|
|
46
|
+
* type behind `client` is not a paths map, inference finds no candidate for
|
|
47
|
+
* `TPaths` and falls back to this constraint, so `createQueryApi` still returns
|
|
48
|
+
* — as `QueryApi<QueryApiPaths>`, on which no `queryOptions` call typechecks.
|
|
49
|
+
* The rejection lands on the calls rather than on the construction.
|
|
50
|
+
*/
|
|
51
|
+
export type QueryApiPaths = Record<
|
|
52
|
+
string,
|
|
53
|
+
// Suppressed for this one line, not the file. `no-explicit-any` ships in the
|
|
54
|
+
// opt-in `react-strict` oxlint preset, and this is the site that preset
|
|
55
|
+
// cannot express an exception for: oxlint's rule takes only `fixToUnknown`
|
|
56
|
+
// and `ignoreRestArgs`, neither of which distinguishes a generic constraint
|
|
57
|
+
// from a value annotation. `unknown` here is not a safer spelling of the same
|
|
58
|
+
// type — it breaks the inference described above and the constraint stops
|
|
59
|
+
// doing its job. The trade is recorded here rather than configured away.
|
|
60
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
61
|
+
{ [TMethod in HttpMethod]?: any } & { parameters?: any }
|
|
62
|
+
>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Methods `queryOptions` accepts: the safe, bodyless, cacheable ones. A
|
|
66
|
+
* POST-backed read (`POST /search`) is deliberately out of v1 — see DESIGN.md.
|
|
67
|
+
*/
|
|
68
|
+
export type QueryApiQueryMethod = Extract<HttpMethod, 'get' | 'head'>;
|
|
69
|
+
|
|
70
|
+
/** Methods `mutationOptions` accepts. */
|
|
71
|
+
export type QueryApiMutationMethod = Extract<
|
|
72
|
+
HttpMethod,
|
|
73
|
+
'post' | 'put' | 'patch' | 'delete'
|
|
74
|
+
>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* What a failed query or mutation rejects with. A request that reached the
|
|
78
|
+
* server rejects with {@link HttpRequestError} (status plus parsed error body);
|
|
79
|
+
* a transport failure or a middleware — response validation, for instance —
|
|
80
|
+
* rejects with whatever it threw. Narrow with `isHttpRequestError` before
|
|
81
|
+
* reading `status`.
|
|
82
|
+
*/
|
|
83
|
+
export type QueryApiError<TErrorBody> = HttpRequestError<TErrorBody> | Error;
|
|
84
|
+
|
|
85
|
+
type InitWithUnknowns<TInit> = TInit & { [key: string]: unknown };
|
|
86
|
+
|
|
87
|
+
type InferSelectReturnType<TData, TSelect> = TSelect extends (
|
|
88
|
+
data: TData,
|
|
89
|
+
) => infer TSelected
|
|
90
|
+
? TSelected
|
|
91
|
+
: TData;
|
|
92
|
+
|
|
93
|
+
/** Per-call options for `queryOptions`: react-query's, plus cache tags. */
|
|
94
|
+
export type QueryApiCallOptions<
|
|
95
|
+
TQueryFnData,
|
|
96
|
+
TError,
|
|
97
|
+
TData,
|
|
98
|
+
TKey extends QueryKey,
|
|
99
|
+
TTag extends string,
|
|
100
|
+
> = Omit<
|
|
101
|
+
UseQueryOptions<TQueryFnData, TError, TData, TKey>,
|
|
102
|
+
'queryKey' | 'queryFn'
|
|
103
|
+
> & {
|
|
104
|
+
/** Tags this endpoint belongs to, for mutation-driven invalidation. */
|
|
105
|
+
tags?: readonly TTag[];
|
|
106
|
+
/** Middleware appended to the instance chain for this call only. */
|
|
107
|
+
middleware?: readonly QueryApiMiddleware[];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A tag to invalidate on mutation success, either fixed or derived from the
|
|
112
|
+
* mutation's own result and variables.
|
|
113
|
+
*/
|
|
114
|
+
export type MutationInvalidation<TTag extends string, TData, TVariables> =
|
|
115
|
+
| TTag
|
|
116
|
+
| ((data: TData, variables: TVariables) => TTag | readonly TTag[]);
|
|
117
|
+
|
|
118
|
+
/** Per-call options for `mutationOptions`: react-query's, plus `invalidates`. */
|
|
119
|
+
export type QueryApiMutationCallOptions<
|
|
120
|
+
TData,
|
|
121
|
+
TError,
|
|
122
|
+
TVariables,
|
|
123
|
+
TOnMutateResult,
|
|
124
|
+
TTag extends string,
|
|
125
|
+
> = Omit<
|
|
126
|
+
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
|
127
|
+
'mutationKey' | 'mutationFn'
|
|
128
|
+
> & {
|
|
129
|
+
invalidates?: readonly MutationInvalidation<TTag, TData, TVariables>[];
|
|
130
|
+
/** Middleware appended to the instance chain for this call only. */
|
|
131
|
+
middleware?: readonly QueryApiMiddleware[];
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type QueryApiKeyFn<
|
|
135
|
+
TPaths extends QueryApiPaths,
|
|
136
|
+
TMedia extends MediaType,
|
|
137
|
+
> = <
|
|
138
|
+
TMethod extends QueryApiQueryMethod,
|
|
139
|
+
TPath extends PathsWithMethod<TPaths, TMethod>,
|
|
140
|
+
TInit extends MaybeOptionalInit<TPaths[TPath], TMethod>,
|
|
141
|
+
TResponse extends Required<
|
|
142
|
+
FetchResponse<TPaths[TPath][TMethod], TInit, TMedia>
|
|
143
|
+
>,
|
|
144
|
+
>(
|
|
145
|
+
method: TMethod,
|
|
146
|
+
path: TPath,
|
|
147
|
+
...[init]: RequiredKeysOf<TInit> extends never
|
|
148
|
+
? [init?: InitWithUnknowns<TInit>]
|
|
149
|
+
: [init: InitWithUnknowns<TInit>]
|
|
150
|
+
// `NoInfer` matters here: without it, a contextual type on the result — the
|
|
151
|
+
// `queryKey` property of an `invalidateQueries` filter, say — becomes an
|
|
152
|
+
// inference site for `TResponse`, which leaves `TInit` unresolved and makes
|
|
153
|
+
// TypeScript demand the optional `init` argument.
|
|
154
|
+
) => NoInfer<
|
|
155
|
+
DataTag<
|
|
156
|
+
QueryApiKey<TMethod, TPath>,
|
|
157
|
+
TResponse['data'],
|
|
158
|
+
QueryApiError<TResponse['error']>
|
|
159
|
+
>
|
|
160
|
+
>;
|
|
161
|
+
|
|
162
|
+
export type QueryApiQueryOptionsFn<
|
|
163
|
+
TPaths extends QueryApiPaths,
|
|
164
|
+
TTag extends string,
|
|
165
|
+
TMedia extends MediaType,
|
|
166
|
+
> = <
|
|
167
|
+
TMethod extends QueryApiQueryMethod,
|
|
168
|
+
TPath extends PathsWithMethod<TPaths, TMethod>,
|
|
169
|
+
TInit extends MaybeOptionalInit<TPaths[TPath], TMethod>,
|
|
170
|
+
// `Required` here so the option types below never repeat `NonNullable`.
|
|
171
|
+
TResponse extends Required<
|
|
172
|
+
FetchResponse<TPaths[TPath][TMethod], TInit, TMedia>
|
|
173
|
+
>,
|
|
174
|
+
TOptions extends QueryApiCallOptions<
|
|
175
|
+
TResponse['data'],
|
|
176
|
+
QueryApiError<TResponse['error']>,
|
|
177
|
+
InferSelectReturnType<TResponse['data'], TOptions['select']>,
|
|
178
|
+
QueryApiKey<TMethod, TPath>,
|
|
179
|
+
TTag
|
|
180
|
+
>,
|
|
181
|
+
>(
|
|
182
|
+
method: TMethod,
|
|
183
|
+
path: TPath,
|
|
184
|
+
...[init, options]: RequiredKeysOf<TInit> extends never
|
|
185
|
+
? [init?: InitWithUnknowns<TInit>, options?: TOptions]
|
|
186
|
+
: [init: InitWithUnknowns<TInit>, options?: TOptions]
|
|
187
|
+
) => NoInfer<
|
|
188
|
+
Omit<
|
|
189
|
+
UseQueryOptions<
|
|
190
|
+
TResponse['data'],
|
|
191
|
+
QueryApiError<TResponse['error']>,
|
|
192
|
+
InferSelectReturnType<TResponse['data'], TOptions['select']>,
|
|
193
|
+
QueryApiKey<TMethod, TPath>
|
|
194
|
+
>,
|
|
195
|
+
'queryKey' | 'queryFn'
|
|
196
|
+
> & {
|
|
197
|
+
queryKey: DataTag<
|
|
198
|
+
QueryApiKey<TMethod, TPath>,
|
|
199
|
+
TResponse['data'],
|
|
200
|
+
QueryApiError<TResponse['error']>
|
|
201
|
+
>;
|
|
202
|
+
queryFn: Exclude<
|
|
203
|
+
UseQueryOptions<
|
|
204
|
+
TResponse['data'],
|
|
205
|
+
QueryApiError<TResponse['error']>,
|
|
206
|
+
InferSelectReturnType<TResponse['data'], TOptions['select']>,
|
|
207
|
+
QueryApiKey<TMethod, TPath>
|
|
208
|
+
>['queryFn'],
|
|
209
|
+
SkipToken | undefined
|
|
210
|
+
>;
|
|
211
|
+
}
|
|
212
|
+
>;
|
|
213
|
+
|
|
214
|
+
export type QueryApiMutationOptionsFn<
|
|
215
|
+
TPaths extends QueryApiPaths,
|
|
216
|
+
TTag extends string,
|
|
217
|
+
TMedia extends MediaType,
|
|
218
|
+
> = <
|
|
219
|
+
TMethod extends QueryApiMutationMethod,
|
|
220
|
+
TPath extends PathsWithMethod<TPaths, TMethod>,
|
|
221
|
+
TInit extends MaybeOptionalInit<TPaths[TPath], TMethod>,
|
|
222
|
+
TResponse extends Required<
|
|
223
|
+
FetchResponse<TPaths[TPath][TMethod], TInit, TMedia>
|
|
224
|
+
>,
|
|
225
|
+
TOnMutateResult = unknown,
|
|
226
|
+
>(
|
|
227
|
+
method: TMethod,
|
|
228
|
+
path: TPath,
|
|
229
|
+
options?: QueryApiMutationCallOptions<
|
|
230
|
+
TResponse['data'],
|
|
231
|
+
QueryApiError<TResponse['error']>,
|
|
232
|
+
InitWithUnknowns<TInit>,
|
|
233
|
+
TOnMutateResult,
|
|
234
|
+
TTag
|
|
235
|
+
>,
|
|
236
|
+
) => NoInfer<
|
|
237
|
+
Omit<
|
|
238
|
+
UseMutationOptions<
|
|
239
|
+
TResponse['data'],
|
|
240
|
+
QueryApiError<TResponse['error']>,
|
|
241
|
+
InitWithUnknowns<TInit>,
|
|
242
|
+
TOnMutateResult
|
|
243
|
+
>,
|
|
244
|
+
'mutationKey' | 'mutationFn'
|
|
245
|
+
> & {
|
|
246
|
+
mutationKey: readonly [method: TMethod, path: TPath];
|
|
247
|
+
mutationFn: NonNullable<
|
|
248
|
+
UseMutationOptions<
|
|
249
|
+
TResponse['data'],
|
|
250
|
+
QueryApiError<TResponse['error']>,
|
|
251
|
+
InitWithUnknowns<TInit>,
|
|
252
|
+
TOnMutateResult
|
|
253
|
+
>['mutationFn']
|
|
254
|
+
>;
|
|
255
|
+
}
|
|
256
|
+
>;
|
|
257
|
+
|
|
258
|
+
/** Instance-level configuration for {@link createQueryApi}. */
|
|
259
|
+
export type QueryApiOptions<TTag extends string> = {
|
|
260
|
+
/**
|
|
261
|
+
* The tag vocabulary. Passing it both infers `TTag` (so `tags` and
|
|
262
|
+
* `invalidates` are checked against a closed set) and makes an unknown tag
|
|
263
|
+
* throw at runtime, which is what catches typos from untyped call sites.
|
|
264
|
+
*/
|
|
265
|
+
tags?: readonly TTag[];
|
|
266
|
+
/** Middleware applied to every request from this instance, outermost first. */
|
|
267
|
+
middleware?: readonly QueryApiMiddleware[];
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export type QueryApi<
|
|
271
|
+
TPaths extends QueryApiPaths,
|
|
272
|
+
TTag extends string = string,
|
|
273
|
+
TMedia extends MediaType = MediaType,
|
|
274
|
+
> = {
|
|
275
|
+
/**
|
|
276
|
+
* The derived key for an operation, for targeting `getQueryData`,
|
|
277
|
+
* `setQueryData`, or `invalidateQueries` without a hand-built key factory.
|
|
278
|
+
* Slice it to `[method, path]` to target every cached variant of an endpoint.
|
|
279
|
+
*/
|
|
280
|
+
queryKey: QueryApiKeyFn<TPaths, TMedia>;
|
|
281
|
+
queryOptions: QueryApiQueryOptionsFn<TPaths, TTag, TMedia>;
|
|
282
|
+
mutationOptions: QueryApiMutationOptionsFn<TPaths, TTag, TMedia>;
|
|
283
|
+
/** A react-query filter matching every query registered under `tag`. */
|
|
284
|
+
tagFilter: (tag: TTag) => QueryFilters;
|
|
285
|
+
invalidateTags: (client: QueryClient, tags: readonly TTag[]) => Promise<void>;
|
|
286
|
+
/** The `${method} ${path}` tokens currently registered under `tag`. */
|
|
287
|
+
taggedEndpoints: (tag: TTag) => readonly string[];
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
type LooseFetchResult = {
|
|
291
|
+
data?: unknown;
|
|
292
|
+
error?: unknown;
|
|
293
|
+
response: Response;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
type LooseClientMethod = (
|
|
297
|
+
path: string,
|
|
298
|
+
init?: Record<string, unknown>,
|
|
299
|
+
) => Promise<LooseFetchResult>;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Binds a TanStack Query surface to an `openapi-fetch` client.
|
|
303
|
+
*
|
|
304
|
+
* The generated `TPaths` type is the only endpoint definition: methods, paths,
|
|
305
|
+
* params, request bodies, and response types are all read off it, and both
|
|
306
|
+
* `TPaths` and the tag vocabulary are inferred from the arguments — prefer
|
|
307
|
+
* `createQueryApi(client, { tags: [...] })` over passing type arguments
|
|
308
|
+
* explicitly, since naming one disables inference for the rest.
|
|
309
|
+
*/
|
|
310
|
+
export function createQueryApi<
|
|
311
|
+
TPaths extends QueryApiPaths,
|
|
312
|
+
TTag extends string = string,
|
|
313
|
+
TMedia extends MediaType = MediaType,
|
|
314
|
+
>(
|
|
315
|
+
client: ApiClient<TPaths, TMedia>,
|
|
316
|
+
options: QueryApiOptions<TTag> = {},
|
|
317
|
+
): QueryApi<TPaths, TTag, TMedia> {
|
|
318
|
+
const registry = createTagRegistry(options.tags);
|
|
319
|
+
const instanceMiddleware = options.middleware ?? [];
|
|
320
|
+
const methods = client as unknown as Record<string, LooseClientMethod>;
|
|
321
|
+
|
|
322
|
+
const send = async (ctx: QueryApiRequestContext): Promise<unknown> => {
|
|
323
|
+
const method = ctx.method.toUpperCase();
|
|
324
|
+
const call = methods[method];
|
|
325
|
+
if (!call) {
|
|
326
|
+
throw new Error(`http-client-react: client has no ${method} method`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const { data, error, response } = await call(ctx.path, ctx.init);
|
|
330
|
+
|
|
331
|
+
if (!response.ok || error !== undefined) {
|
|
332
|
+
throw new HttpRequestError({
|
|
333
|
+
method: ctx.method,
|
|
334
|
+
path: ctx.path,
|
|
335
|
+
body: error,
|
|
336
|
+
response,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// A HEAD response has no body by definition, so `openapi-fetch` resolves
|
|
341
|
+
// `data: undefined` whatever the status and whatever `Content-Length`
|
|
342
|
+
// claims — and it claims the size the matching GET would have returned, so
|
|
343
|
+
// the emptiness checks below do not catch it. react-query rejects
|
|
344
|
+
// `undefined` as query data.
|
|
345
|
+
if (method === 'HEAD') return data ?? null;
|
|
346
|
+
|
|
347
|
+
// Same conversion for the other two ways a 2xx legitimately has no body.
|
|
348
|
+
if (
|
|
349
|
+
response.status === 204 ||
|
|
350
|
+
response.headers.get('Content-Length') === '0'
|
|
351
|
+
) {
|
|
352
|
+
return data ?? null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return data;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const request = (
|
|
359
|
+
operationType: QueryApiOperationType,
|
|
360
|
+
method: string,
|
|
361
|
+
path: string,
|
|
362
|
+
init: Record<string, unknown> | undefined,
|
|
363
|
+
callMiddleware: readonly QueryApiMiddleware[] | undefined,
|
|
364
|
+
): Promise<unknown> => {
|
|
365
|
+
const chain = callMiddleware?.length
|
|
366
|
+
? [...instanceMiddleware, ...callMiddleware]
|
|
367
|
+
: instanceMiddleware;
|
|
368
|
+
|
|
369
|
+
return composeMiddleware(
|
|
370
|
+
chain,
|
|
371
|
+
send,
|
|
372
|
+
)({
|
|
373
|
+
method,
|
|
374
|
+
path,
|
|
375
|
+
operationType,
|
|
376
|
+
init,
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const tagFilter = (tag: TTag): QueryFilters => {
|
|
381
|
+
registry.assertKnown(tag);
|
|
382
|
+
return { predicate: (query) => registry.matches(tag, query.queryKey) };
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const invalidateTags = async (
|
|
386
|
+
client_: QueryClient,
|
|
387
|
+
tags: readonly TTag[],
|
|
388
|
+
): Promise<void> => {
|
|
389
|
+
await Promise.all(
|
|
390
|
+
tags.map((tag) => client_.invalidateQueries(tagFilter(tag))),
|
|
391
|
+
);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Runs after the mutation has already succeeded, which is what makes the
|
|
396
|
+
* asymmetry below the right one: a literal tag was checked when
|
|
397
|
+
* `mutationOptions` was called, so it can still throw; a tag a callback
|
|
398
|
+
* derives from the result can only be checked here, and throwing here would
|
|
399
|
+
* turn a successful mutation into a failed one and skip the caller's own
|
|
400
|
+
* `onSuccess`. So an unknown derived tag is warned about and dropped.
|
|
401
|
+
*/
|
|
402
|
+
const resolveInvalidations = (
|
|
403
|
+
invalidates: readonly MutationInvalidation<TTag, never, never>[],
|
|
404
|
+
data: unknown,
|
|
405
|
+
variables: unknown,
|
|
406
|
+
): readonly TTag[] => {
|
|
407
|
+
const resolved = new Set<TTag>();
|
|
408
|
+
|
|
409
|
+
for (const entry of invalidates) {
|
|
410
|
+
if (typeof entry !== 'function') {
|
|
411
|
+
resolved.add(entry);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const produced = (
|
|
416
|
+
entry as (d: unknown, v: unknown) => TTag | readonly TTag[]
|
|
417
|
+
)(data, variables);
|
|
418
|
+
|
|
419
|
+
for (const tag of typeof produced === 'string' ? [produced] : produced) {
|
|
420
|
+
if (registry.acceptOrWarn(tag)) resolved.add(tag);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return [...resolved];
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The public surface is the generic type above; the implementation is written
|
|
429
|
+
* against loose types and cast once, here. `query-api.types.test.ts` is what
|
|
430
|
+
* holds the two in agreement.
|
|
431
|
+
*/
|
|
432
|
+
const api = {
|
|
433
|
+
queryKey: (method: string, path: string, init?: Record<string, unknown>) =>
|
|
434
|
+
buildQueryApiKey(method, path, init),
|
|
435
|
+
|
|
436
|
+
queryOptions: (
|
|
437
|
+
method: string,
|
|
438
|
+
path: string,
|
|
439
|
+
init?: Record<string, unknown>,
|
|
440
|
+
callOptions?: QueryApiCallOptions<
|
|
441
|
+
unknown,
|
|
442
|
+
Error,
|
|
443
|
+
unknown,
|
|
444
|
+
QueryKey,
|
|
445
|
+
TTag
|
|
446
|
+
>,
|
|
447
|
+
) => {
|
|
448
|
+
const { tags, middleware, ...queryRest } = callOptions ?? {};
|
|
449
|
+
|
|
450
|
+
for (const tag of tags ?? []) registry.register(tag, method, path);
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
...queryRest,
|
|
454
|
+
queryKey: buildQueryApiKey(method, path, init),
|
|
455
|
+
queryFn: ({ signal }: { signal: AbortSignal }) =>
|
|
456
|
+
request('query', method, path, { ...init, signal }, middleware),
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
mutationOptions: (
|
|
461
|
+
method: string,
|
|
462
|
+
path: string,
|
|
463
|
+
callOptions?: QueryApiMutationCallOptions<
|
|
464
|
+
never,
|
|
465
|
+
Error,
|
|
466
|
+
never,
|
|
467
|
+
unknown,
|
|
468
|
+
TTag
|
|
469
|
+
>,
|
|
470
|
+
) => {
|
|
471
|
+
const { invalidates, middleware, onSuccess, ...mutationRest } =
|
|
472
|
+
callOptions ?? {};
|
|
473
|
+
|
|
474
|
+
for (const entry of invalidates ?? []) {
|
|
475
|
+
if (typeof entry === 'string') registry.assertKnown(entry);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
...mutationRest,
|
|
480
|
+
mutationKey: [method, path],
|
|
481
|
+
mutationFn: (variables?: Record<string, unknown>) =>
|
|
482
|
+
request('mutation', method, path, variables, middleware),
|
|
483
|
+
onSuccess: async (
|
|
484
|
+
data: never,
|
|
485
|
+
variables: never,
|
|
486
|
+
onMutateResult: unknown,
|
|
487
|
+
context: { client: QueryClient },
|
|
488
|
+
) => {
|
|
489
|
+
if (invalidates?.length) {
|
|
490
|
+
await invalidateTags(
|
|
491
|
+
context.client,
|
|
492
|
+
resolveInvalidations(invalidates, data, variables),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return await onSuccess?.(
|
|
497
|
+
data,
|
|
498
|
+
variables,
|
|
499
|
+
onMutateResult as never,
|
|
500
|
+
context as never,
|
|
501
|
+
);
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
tagFilter,
|
|
507
|
+
invalidateTags,
|
|
508
|
+
taggedEndpoints: registry.endpoints,
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
return api as unknown as QueryApi<TPaths, TTag, TMedia>;
|
|
512
|
+
}
|