@beignet/react-query 0.0.2 → 0.0.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 +43 -0
- package/README.md +175 -37
- package/dist/index.d.ts +108 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +140 -9
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
- package/src/index.ts +293 -44
package/src/index.ts
CHANGED
|
@@ -2,7 +2,10 @@ import type {
|
|
|
2
2
|
Client,
|
|
3
3
|
Endpoint,
|
|
4
4
|
EndpointCallArgs,
|
|
5
|
+
InferBody,
|
|
5
6
|
InferEndpointContractError,
|
|
7
|
+
InferPathParams,
|
|
8
|
+
InferQuery,
|
|
6
9
|
InferSuccessResponse,
|
|
7
10
|
} from "@beignet/core/client";
|
|
8
11
|
import {
|
|
@@ -14,22 +17,50 @@ import {
|
|
|
14
17
|
|
|
15
18
|
import type {
|
|
16
19
|
InfiniteQueryObserverOptions,
|
|
17
|
-
|
|
20
|
+
InvalidateQueryFilters,
|
|
21
|
+
QueryClient,
|
|
22
|
+
QueryFunction,
|
|
18
23
|
QueryKey,
|
|
19
|
-
|
|
24
|
+
UseMutationOptions,
|
|
25
|
+
UseQueryOptions,
|
|
20
26
|
} from "@tanstack/react-query";
|
|
21
27
|
|
|
22
|
-
type HasRequiredKeys<T> = {
|
|
23
|
-
[K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
|
|
24
|
-
}[keyof T];
|
|
25
|
-
|
|
26
28
|
type EndpointQueryArgs<
|
|
27
29
|
TContract extends HttpContractConfig,
|
|
28
30
|
TProvidedHeaders extends string,
|
|
29
|
-
> = Omit<
|
|
31
|
+
> = Omit<
|
|
32
|
+
EndpointCallArgs<TContract, TProvidedHeaders>,
|
|
33
|
+
"rawBody" | "signal" | "idempotencyKey"
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
type EndpointArgValue<
|
|
37
|
+
TArgs,
|
|
38
|
+
TKey extends "path" | "query" | "body" | "headers",
|
|
39
|
+
> = TKey extends keyof TArgs ? TArgs[TKey] : undefined;
|
|
40
|
+
|
|
41
|
+
type IsRequiredValue<T> = undefined extends T ? false : true;
|
|
42
|
+
|
|
43
|
+
type RequiresQueryOptionsArgs<
|
|
44
|
+
TContract extends HttpContractConfig,
|
|
45
|
+
TProvidedHeaders extends string,
|
|
46
|
+
> =
|
|
47
|
+
IsRequiredValue<InferPathParams<TContract>> extends true
|
|
48
|
+
? true
|
|
49
|
+
: IsRequiredValue<InferQuery<TContract>> extends true
|
|
50
|
+
? true
|
|
51
|
+
: IsRequiredValue<InferBody<TContract>> extends true
|
|
52
|
+
? true
|
|
53
|
+
: IsRequiredValue<
|
|
54
|
+
EndpointArgValue<
|
|
55
|
+
EndpointCallArgs<TContract, TProvidedHeaders>,
|
|
56
|
+
"headers"
|
|
57
|
+
>
|
|
58
|
+
> extends true
|
|
59
|
+
? true
|
|
60
|
+
: false;
|
|
30
61
|
|
|
31
62
|
type ContractQueryOptionsBase<TContract extends HttpContractConfig> = Omit<
|
|
32
|
-
|
|
63
|
+
UseQueryOptions<
|
|
33
64
|
InferSuccessResponse<TContract>,
|
|
34
65
|
InferEndpointContractError<TContract>,
|
|
35
66
|
InferSuccessResponse<TContract>,
|
|
@@ -38,6 +69,12 @@ type ContractQueryOptionsBase<TContract extends HttpContractConfig> = Omit<
|
|
|
38
69
|
"queryKey" | "queryFn"
|
|
39
70
|
>;
|
|
40
71
|
|
|
72
|
+
type ContractQueryOptionsResult<TContract extends HttpContractConfig> =
|
|
73
|
+
ContractQueryOptionsBase<TContract> & {
|
|
74
|
+
queryKey: QueryKey;
|
|
75
|
+
queryFn: QueryFunction<InferSuccessResponse<TContract>, QueryKey>;
|
|
76
|
+
};
|
|
77
|
+
|
|
41
78
|
/**
|
|
42
79
|
* Options accepted by `ReactQueryContractHelper.queryOptions(...)`.
|
|
43
80
|
*
|
|
@@ -56,9 +93,31 @@ type ContractQueryOptionsCallArgs<
|
|
|
56
93
|
TContract extends HttpContractConfig,
|
|
57
94
|
TProvidedHeaders extends string,
|
|
58
95
|
> =
|
|
59
|
-
|
|
60
|
-
? [args
|
|
61
|
-
: [args
|
|
96
|
+
RequiresQueryOptionsArgs<TContract, TProvidedHeaders> extends true
|
|
97
|
+
? [args: ContractUseQueryOptions<TContract, TProvidedHeaders>]
|
|
98
|
+
: [args?: ContractUseQueryOptions<TContract, TProvidedHeaders>];
|
|
99
|
+
|
|
100
|
+
export type ContractQueryKeyParams<
|
|
101
|
+
TContract extends HttpContractConfig,
|
|
102
|
+
TProvidedHeaders extends string = never,
|
|
103
|
+
> = {
|
|
104
|
+
path?: EndpointArgValue<
|
|
105
|
+
EndpointQueryArgs<TContract, TProvidedHeaders>,
|
|
106
|
+
"path"
|
|
107
|
+
>;
|
|
108
|
+
query?: EndpointArgValue<
|
|
109
|
+
EndpointQueryArgs<TContract, TProvidedHeaders>,
|
|
110
|
+
"query"
|
|
111
|
+
>;
|
|
112
|
+
body?: EndpointArgValue<
|
|
113
|
+
EndpointQueryArgs<TContract, TProvidedHeaders>,
|
|
114
|
+
"body"
|
|
115
|
+
>;
|
|
116
|
+
headers?: EndpointArgValue<
|
|
117
|
+
EndpointQueryArgs<TContract, TProvidedHeaders>,
|
|
118
|
+
"headers"
|
|
119
|
+
>;
|
|
120
|
+
};
|
|
62
121
|
|
|
63
122
|
/**
|
|
64
123
|
* Options accepted by `ReactQueryContractHelper.mutationOptions(...)`.
|
|
@@ -70,7 +129,7 @@ export type ContractUseMutationOptions<
|
|
|
70
129
|
TContract extends HttpContractConfig,
|
|
71
130
|
TProvidedHeaders extends string = never,
|
|
72
131
|
> = Omit<
|
|
73
|
-
|
|
132
|
+
UseMutationOptions<
|
|
74
133
|
InferSuccessResponse<TContract>,
|
|
75
134
|
InferEndpointContractError<TContract>,
|
|
76
135
|
EndpointCallArgs<TContract, TProvidedHeaders>,
|
|
@@ -79,12 +138,26 @@ export type ContractUseMutationOptions<
|
|
|
79
138
|
"mutationFn"
|
|
80
139
|
>;
|
|
81
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Body input for infinite queries.
|
|
143
|
+
*
|
|
144
|
+
* Object bodies are shallow-partial because the full request body is the
|
|
145
|
+
* merge of the static `body` and each `page(...).body`, so neither side has
|
|
146
|
+
* to satisfy required body fields alone. The merged body is still validated
|
|
147
|
+
* by the endpoint call.
|
|
148
|
+
*/
|
|
149
|
+
type InfiniteQueryBody<TContract extends HttpContractConfig> =
|
|
150
|
+
InferBody<TContract> extends Record<string, unknown>
|
|
151
|
+
? Partial<InferBody<TContract>>
|
|
152
|
+
: EndpointCallArgs<TContract>["body"];
|
|
153
|
+
|
|
82
154
|
type InfiniteQueryResolvedParams<
|
|
83
155
|
TContract extends HttpContractConfig,
|
|
84
156
|
TProvidedHeaders extends string = never,
|
|
85
157
|
> = {
|
|
86
158
|
path?: EndpointCallArgs<TContract>["path"];
|
|
87
159
|
query?: EndpointCallArgs<TContract>["query"];
|
|
160
|
+
body?: InfiniteQueryBody<TContract>;
|
|
88
161
|
headers?: EndpointCallArgs<TContract, TProvidedHeaders>["headers"];
|
|
89
162
|
};
|
|
90
163
|
|
|
@@ -95,6 +168,7 @@ type SafeInfiniteQueryArgs<
|
|
|
95
168
|
> = {
|
|
96
169
|
path?: EndpointCallArgs<TContract>["path"];
|
|
97
170
|
query?: EndpointCallArgs<TContract>["query"];
|
|
171
|
+
body?: InfiniteQueryBody<TContract>;
|
|
98
172
|
headers?: EndpointCallArgs<TContract, TProvidedHeaders>["headers"];
|
|
99
173
|
page?: (ctx: {
|
|
100
174
|
pageParam: TPageParam;
|
|
@@ -114,6 +188,7 @@ type DynamicInfiniteQueryArgs<
|
|
|
114
188
|
key: readonly unknown[];
|
|
115
189
|
path?: never;
|
|
116
190
|
query?: never;
|
|
191
|
+
body?: never;
|
|
117
192
|
page?: never;
|
|
118
193
|
};
|
|
119
194
|
|
|
@@ -140,6 +215,15 @@ function mergeParamObjects<T>(base?: T, patch?: T): T | undefined {
|
|
|
140
215
|
if (base == null) return patch;
|
|
141
216
|
if (patch == null) return base;
|
|
142
217
|
|
|
218
|
+
if (
|
|
219
|
+
typeof base !== "object" ||
|
|
220
|
+
typeof patch !== "object" ||
|
|
221
|
+
Array.isArray(base) ||
|
|
222
|
+
Array.isArray(patch)
|
|
223
|
+
) {
|
|
224
|
+
return patch;
|
|
225
|
+
}
|
|
226
|
+
|
|
143
227
|
return {
|
|
144
228
|
...(base as Record<string, unknown>),
|
|
145
229
|
...(patch as Record<string, unknown>),
|
|
@@ -166,6 +250,16 @@ function normalizeKeyValue(value: unknown): unknown | undefined {
|
|
|
166
250
|
const BEIGNET_QUERY_KEY_SCOPE = "beignet";
|
|
167
251
|
const UNNAMESPACED_QUERY_KEY_NAMESPACE = null;
|
|
168
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Idempotency keys generated per `mutate(...)` invocation, keyed by variables
|
|
255
|
+
* object identity.
|
|
256
|
+
*
|
|
257
|
+
* TanStack Query re-invokes `mutationFn` with the same variables object on
|
|
258
|
+
* every HTTP retry attempt, so one key per variables object means retries
|
|
259
|
+
* reuse the key while separate `mutate(...)` calls get fresh keys.
|
|
260
|
+
*/
|
|
261
|
+
const idempotencyKeyForVariables = new WeakMap<object, string>();
|
|
262
|
+
|
|
169
263
|
/**
|
|
170
264
|
* Query key for all contracts in one Beignet namespace.
|
|
171
265
|
*/
|
|
@@ -176,11 +270,15 @@ export type ContractQueryNamespaceKey = readonly [
|
|
|
176
270
|
|
|
177
271
|
/**
|
|
178
272
|
* Query key for one contract without path/query/body params.
|
|
273
|
+
*
|
|
274
|
+
* The fourth element is the contract route (`"GET /todos"`), so contracts
|
|
275
|
+
* with the same derived local name but different routes never share a key.
|
|
179
276
|
*/
|
|
180
277
|
export type ContractQueryContractKey = readonly [
|
|
181
278
|
typeof BEIGNET_QUERY_KEY_SCOPE,
|
|
182
279
|
string | null,
|
|
183
280
|
string,
|
|
281
|
+
string,
|
|
184
282
|
];
|
|
185
283
|
|
|
186
284
|
/**
|
|
@@ -190,9 +288,36 @@ export type ContractQueryKey = readonly [
|
|
|
190
288
|
typeof BEIGNET_QUERY_KEY_SCOPE,
|
|
191
289
|
string | null,
|
|
192
290
|
string,
|
|
291
|
+
string,
|
|
193
292
|
unknown?,
|
|
194
293
|
];
|
|
195
294
|
|
|
295
|
+
export type ContractQueryFilterOptions = Omit<
|
|
296
|
+
InvalidateQueryFilters<QueryKey>,
|
|
297
|
+
"queryKey"
|
|
298
|
+
>;
|
|
299
|
+
|
|
300
|
+
export type ContractQueryFilter<TKey extends QueryKey> =
|
|
301
|
+
ContractQueryFilterOptions & {
|
|
302
|
+
queryKey: TKey;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Options accepted by `createReactQuery(client, options?)`.
|
|
307
|
+
*/
|
|
308
|
+
export type CreateReactQueryOptions = {
|
|
309
|
+
/**
|
|
310
|
+
* Header names to include in generated query keys.
|
|
311
|
+
*
|
|
312
|
+
* Headers are excluded from query keys by default because persisted caches
|
|
313
|
+
* would otherwise store credentials such as `Authorization` tokens. Opt in
|
|
314
|
+
* per adapter with the specific identity headers that change response data,
|
|
315
|
+
* such as a tenant header. Names are matched case-insensitively and stored
|
|
316
|
+
* lowercased in the key.
|
|
317
|
+
*/
|
|
318
|
+
keyHeaders?: readonly string[];
|
|
319
|
+
};
|
|
320
|
+
|
|
196
321
|
/**
|
|
197
322
|
* TanStack Query helper bound to one Beignet contract.
|
|
198
323
|
*
|
|
@@ -207,6 +332,7 @@ export class ReactQueryContractHelper<
|
|
|
207
332
|
constructor(
|
|
208
333
|
private contract: TContract,
|
|
209
334
|
private _endpoint: Endpoint<TContract, TProvidedHeaders>,
|
|
335
|
+
private options: CreateReactQueryOptions = {},
|
|
210
336
|
) {}
|
|
211
337
|
|
|
212
338
|
/**
|
|
@@ -216,6 +342,14 @@ export class ReactQueryContractHelper<
|
|
|
216
342
|
return this.contract.name;
|
|
217
343
|
}
|
|
218
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Contract route used as the key segment that disambiguates contracts with
|
|
347
|
+
* the same local name, such as `/v1/todos` and `/v2/todos` list contracts.
|
|
348
|
+
*/
|
|
349
|
+
get route(): string {
|
|
350
|
+
return `${this.contract.method} ${this.contract.path}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
219
353
|
/**
|
|
220
354
|
* Resource namespace used for query-key grouping.
|
|
221
355
|
*/
|
|
@@ -241,7 +375,68 @@ export class ReactQueryContractHelper<
|
|
|
241
375
|
* Build a contract-level query key without path/query params.
|
|
242
376
|
*/
|
|
243
377
|
contractKey(): ContractQueryContractKey {
|
|
244
|
-
return [
|
|
378
|
+
return [
|
|
379
|
+
BEIGNET_QUERY_KEY_SCOPE,
|
|
380
|
+
this.namespace,
|
|
381
|
+
this.localName,
|
|
382
|
+
this.route,
|
|
383
|
+
] as const;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Build TanStack Query filters for all contracts in this namespace.
|
|
388
|
+
*/
|
|
389
|
+
namespaceFilter(
|
|
390
|
+
options: ContractQueryFilterOptions = {},
|
|
391
|
+
): ContractQueryFilter<ContractQueryNamespaceKey> {
|
|
392
|
+
return {
|
|
393
|
+
...options,
|
|
394
|
+
queryKey: this.namespaceKey(),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Build TanStack Query filters for every cached call to this contract.
|
|
400
|
+
*/
|
|
401
|
+
contractFilter(
|
|
402
|
+
options: ContractQueryFilterOptions = {},
|
|
403
|
+
): ContractQueryFilter<ContractQueryContractKey> {
|
|
404
|
+
return {
|
|
405
|
+
...options,
|
|
406
|
+
queryKey: this.contractKey(),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Pick the adapter-whitelisted `keyHeaders` out of call headers.
|
|
412
|
+
*
|
|
413
|
+
* Returns `undefined` unless the adapter opted in with `keyHeaders` and the
|
|
414
|
+
* call provides at least one whitelisted header. Header names are matched
|
|
415
|
+
* case-insensitively and lowercased in the key component.
|
|
416
|
+
*/
|
|
417
|
+
private pickKeyHeaders(
|
|
418
|
+
headers: unknown,
|
|
419
|
+
): Record<string, unknown> | undefined {
|
|
420
|
+
const keyHeaders = this.options.keyHeaders;
|
|
421
|
+
if (!keyHeaders || keyHeaders.length === 0) return undefined;
|
|
422
|
+
if (
|
|
423
|
+
headers === null ||
|
|
424
|
+
headers === undefined ||
|
|
425
|
+
typeof headers !== "object" ||
|
|
426
|
+
Array.isArray(headers)
|
|
427
|
+
) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const allowed = new Set(keyHeaders.map((name) => name.toLowerCase()));
|
|
432
|
+
const picked: Record<string, unknown> = {};
|
|
433
|
+
for (const [name, value] of Object.entries(
|
|
434
|
+
headers as Record<string, unknown>,
|
|
435
|
+
)) {
|
|
436
|
+
const lowered = name.toLowerCase();
|
|
437
|
+
if (allowed.has(lowered)) picked[lowered] = value;
|
|
438
|
+
}
|
|
439
|
+
return picked;
|
|
245
440
|
}
|
|
246
441
|
|
|
247
442
|
/**
|
|
@@ -251,17 +446,28 @@ export class ReactQueryContractHelper<
|
|
|
251
446
|
* This ensures consistent serialization between server (dehydrate) and
|
|
252
447
|
* client (hydrate), since undefined object values may serialize
|
|
253
448
|
* differently across React's RSC boundary vs JSON.stringify.
|
|
449
|
+
*
|
|
450
|
+
* Headers are never included unless the adapter opted in with
|
|
451
|
+
* `createReactQuery(client, { keyHeaders })`, and then only the whitelisted
|
|
452
|
+
* header names are included.
|
|
254
453
|
*/
|
|
255
454
|
key(params?: {
|
|
256
455
|
path?: unknown;
|
|
257
456
|
query?: unknown;
|
|
258
457
|
body?: unknown;
|
|
458
|
+
headers?: unknown;
|
|
259
459
|
}): ContractQueryKey {
|
|
260
460
|
const path = normalizeKeyValue(params?.path);
|
|
261
461
|
const query = normalizeKeyValue(params?.query);
|
|
262
462
|
const body = normalizeKeyValue(params?.body);
|
|
263
|
-
|
|
264
|
-
|
|
463
|
+
const headers = normalizeKeyValue(this.pickKeyHeaders(params?.headers));
|
|
464
|
+
|
|
465
|
+
if (
|
|
466
|
+
path === undefined &&
|
|
467
|
+
query === undefined &&
|
|
468
|
+
body === undefined &&
|
|
469
|
+
headers === undefined
|
|
470
|
+
) {
|
|
265
471
|
return this.contractKey();
|
|
266
472
|
}
|
|
267
473
|
|
|
@@ -269,9 +475,37 @@ export class ReactQueryContractHelper<
|
|
|
269
475
|
if (path !== undefined) cleanParams.path = path;
|
|
270
476
|
if (query !== undefined) cleanParams.query = query;
|
|
271
477
|
if (body !== undefined) cleanParams.body = body;
|
|
478
|
+
if (headers !== undefined) cleanParams.headers = headers;
|
|
272
479
|
return [...this.contractKey(), cleanParams] as const;
|
|
273
480
|
}
|
|
274
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Build TanStack Query filters for one contract call or parameter prefix.
|
|
484
|
+
*/
|
|
485
|
+
filter(
|
|
486
|
+
params?: ContractQueryKeyParams<TContract, TProvidedHeaders>,
|
|
487
|
+
options: ContractQueryFilterOptions = {},
|
|
488
|
+
): ContractQueryFilter<ContractQueryKey> {
|
|
489
|
+
return {
|
|
490
|
+
...options,
|
|
491
|
+
queryKey: this.key(params),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Invalidate cached data for this contract.
|
|
497
|
+
*
|
|
498
|
+
* With no params this invalidates every cached call to the contract. Pass
|
|
499
|
+
* path/query/body params to target a single call or parameter prefix.
|
|
500
|
+
*/
|
|
501
|
+
invalidate(
|
|
502
|
+
queryClient: QueryClient,
|
|
503
|
+
params?: ContractQueryKeyParams<TContract, TProvidedHeaders>,
|
|
504
|
+
options?: ContractQueryFilterOptions,
|
|
505
|
+
): Promise<void> {
|
|
506
|
+
return queryClient.invalidateQueries(this.filter(params, options));
|
|
507
|
+
}
|
|
508
|
+
|
|
275
509
|
/**
|
|
276
510
|
* Create query options for TanStack Query.
|
|
277
511
|
*
|
|
@@ -280,19 +514,15 @@ export class ReactQueryContractHelper<
|
|
|
280
514
|
*/
|
|
281
515
|
queryOptions(
|
|
282
516
|
...callArgs: ContractQueryOptionsCallArgs<TContract, TProvidedHeaders>
|
|
283
|
-
):
|
|
284
|
-
InferSuccessResponse<TContract>,
|
|
285
|
-
InferEndpointContractError<TContract>,
|
|
286
|
-
InferSuccessResponse<TContract>,
|
|
287
|
-
QueryKey
|
|
288
|
-
> & { queryKey: QueryKey } {
|
|
517
|
+
): ContractQueryOptionsResult<TContract> {
|
|
289
518
|
const args = (callArgs[0] ?? {}) as ContractUseQueryOptions<
|
|
290
519
|
TContract,
|
|
291
520
|
TProvidedHeaders
|
|
292
521
|
>;
|
|
293
522
|
const { path, query, body, headers, key: customKey, ...rest } = args;
|
|
294
523
|
|
|
295
|
-
const queryKey = (customKey ||
|
|
524
|
+
const queryKey = (customKey ||
|
|
525
|
+
this.key({ path, query, body, headers })) as QueryKey;
|
|
296
526
|
|
|
297
527
|
const queryFn = async (context: { signal?: AbortSignal }) => {
|
|
298
528
|
return await this._endpoint.call({
|
|
@@ -308,12 +538,7 @@ export class ReactQueryContractHelper<
|
|
|
308
538
|
...rest,
|
|
309
539
|
queryKey,
|
|
310
540
|
queryFn,
|
|
311
|
-
} as
|
|
312
|
-
InferSuccessResponse<TContract>,
|
|
313
|
-
InferEndpointContractError<TContract>,
|
|
314
|
-
InferSuccessResponse<TContract>,
|
|
315
|
-
QueryKey
|
|
316
|
-
> & { queryKey: QueryKey };
|
|
541
|
+
} as ContractQueryOptionsResult<TContract>;
|
|
317
542
|
}
|
|
318
543
|
|
|
319
544
|
/**
|
|
@@ -321,33 +546,50 @@ export class ReactQueryContractHelper<
|
|
|
321
546
|
*
|
|
322
547
|
* The generated `mutationFn` accepts the same variables shape as the
|
|
323
548
|
* underlying contract endpoint call.
|
|
549
|
+
*
|
|
550
|
+
* For contracts with idempotency metadata, the generated `mutationFn`
|
|
551
|
+
* derives one idempotency key per `mutate(...)` invocation and keeps it
|
|
552
|
+
* stable across TanStack retry attempts, which re-invoke `mutationFn` with
|
|
553
|
+
* the same variables object. Pass `idempotencyKey` in the variables to
|
|
554
|
+
* control the key explicitly. Calling `mutate()` with no variables falls
|
|
555
|
+
* back to per-attempt key generation in the client.
|
|
324
556
|
*/
|
|
325
557
|
mutationOptions(
|
|
326
|
-
args:
|
|
327
|
-
|
|
328
|
-
InferSuccessResponse<TContract>,
|
|
329
|
-
InferEndpointContractError<TContract>,
|
|
330
|
-
EndpointCallArgs<TContract, TProvidedHeaders>,
|
|
331
|
-
unknown
|
|
332
|
-
>,
|
|
333
|
-
"mutationFn"
|
|
334
|
-
> = {},
|
|
335
|
-
): MutationOptions<
|
|
558
|
+
args: ContractUseMutationOptions<TContract, TProvidedHeaders> = {},
|
|
559
|
+
): UseMutationOptions<
|
|
336
560
|
InferSuccessResponse<TContract>,
|
|
337
561
|
InferEndpointContractError<TContract>,
|
|
338
562
|
EndpointCallArgs<TContract, TProvidedHeaders>,
|
|
339
563
|
unknown
|
|
340
564
|
> {
|
|
565
|
+
const hasIdempotencyMetadata = Boolean(this.contract.metadata?.idempotency);
|
|
341
566
|
const mutationFn = async (
|
|
342
567
|
vars: EndpointCallArgs<TContract, TProvidedHeaders>,
|
|
343
568
|
) => {
|
|
344
|
-
|
|
569
|
+
let idempotencyKey = vars?.idempotencyKey;
|
|
570
|
+
if (
|
|
571
|
+
!idempotencyKey &&
|
|
572
|
+
hasIdempotencyMetadata &&
|
|
573
|
+
vars &&
|
|
574
|
+
typeof vars === "object"
|
|
575
|
+
) {
|
|
576
|
+
idempotencyKey =
|
|
577
|
+
idempotencyKeyForVariables.get(vars) ?? crypto.randomUUID();
|
|
578
|
+
idempotencyKeyForVariables.set(vars, idempotencyKey);
|
|
579
|
+
}
|
|
580
|
+
if (idempotencyKey === undefined) {
|
|
581
|
+
return await this._endpoint.call(vars);
|
|
582
|
+
}
|
|
583
|
+
return await this._endpoint.call({
|
|
584
|
+
...vars,
|
|
585
|
+
idempotencyKey,
|
|
586
|
+
} as EndpointCallArgs<TContract, TProvidedHeaders>);
|
|
345
587
|
};
|
|
346
588
|
|
|
347
589
|
return {
|
|
348
590
|
...args,
|
|
349
591
|
mutationFn,
|
|
350
|
-
} as
|
|
592
|
+
} as UseMutationOptions<
|
|
351
593
|
InferSuccessResponse<TContract>,
|
|
352
594
|
InferEndpointContractError<TContract>,
|
|
353
595
|
EndpointCallArgs<TContract, TProvidedHeaders>,
|
|
@@ -374,7 +616,7 @@ export class ReactQueryContractHelper<
|
|
|
374
616
|
let queryKey: QueryKey;
|
|
375
617
|
let rest: Omit<
|
|
376
618
|
ContractInfiniteQueryOptions<TContract, TPageParam, TProvidedHeaders>,
|
|
377
|
-
"path" | "query" | "headers" | "page" | "params" | "key"
|
|
619
|
+
"path" | "query" | "body" | "headers" | "page" | "params" | "key"
|
|
378
620
|
>;
|
|
379
621
|
let resolveParams: (context: {
|
|
380
622
|
pageParam: TPageParam;
|
|
@@ -391,14 +633,15 @@ export class ReactQueryContractHelper<
|
|
|
391
633
|
rest = other;
|
|
392
634
|
resolveParams = params;
|
|
393
635
|
} else {
|
|
394
|
-
const { path, query, headers, page, key, ...other } = args;
|
|
395
|
-
queryKey = (key || this.key({ path, query })) as QueryKey;
|
|
636
|
+
const { path, query, body, headers, page, key, ...other } = args;
|
|
637
|
+
queryKey = (key || this.key({ path, query, body, headers })) as QueryKey;
|
|
396
638
|
rest = other;
|
|
397
639
|
resolveParams = (context) => {
|
|
398
640
|
const pageParams = page?.(context);
|
|
399
641
|
return {
|
|
400
642
|
path: mergeParamObjects(path, pageParams?.path),
|
|
401
643
|
query: mergeParamObjects(query, pageParams?.query),
|
|
644
|
+
body: mergeParamObjects(body, pageParams?.body),
|
|
402
645
|
headers: mergeParamObjects(headers, pageParams?.headers),
|
|
403
646
|
};
|
|
404
647
|
};
|
|
@@ -412,6 +655,7 @@ export class ReactQueryContractHelper<
|
|
|
412
655
|
return await this._endpoint.call({
|
|
413
656
|
path: resolvedParams.path,
|
|
414
657
|
query: resolvedParams.query,
|
|
658
|
+
body: resolvedParams.body,
|
|
415
659
|
headers: resolvedParams.headers,
|
|
416
660
|
signal: context.signal,
|
|
417
661
|
} as unknown as EndpointCallArgs<TContract, TProvidedHeaders>);
|
|
@@ -443,9 +687,14 @@ export class ReactQueryContractHelper<
|
|
|
443
687
|
*
|
|
444
688
|
* Create this once near client setup, then bind contracts with the returned
|
|
445
689
|
* `rq(contract)` function.
|
|
690
|
+
*
|
|
691
|
+
* Pass `keyHeaders` to include specific identity headers, such as a tenant
|
|
692
|
+
* header, in generated query keys. Headers are excluded by default so
|
|
693
|
+
* persisted caches never store credentials.
|
|
446
694
|
*/
|
|
447
695
|
export function createReactQuery<TProvidedHeaders extends string = never>(
|
|
448
696
|
client: Client<TProvidedHeaders>,
|
|
697
|
+
options: CreateReactQueryOptions = {},
|
|
449
698
|
) {
|
|
450
699
|
return function rq<TContractLike extends ContractLike>(
|
|
451
700
|
contract: TContractLike,
|
|
@@ -458,6 +707,6 @@ export function createReactQuery<TProvidedHeaders extends string = never>(
|
|
|
458
707
|
ResolveContract<TContractLike>,
|
|
459
708
|
TProvidedHeaders
|
|
460
709
|
>;
|
|
461
|
-
return new ReactQueryContractHelper(resolved, endpoint);
|
|
710
|
+
return new ReactQueryContractHelper(resolved, endpoint, options);
|
|
462
711
|
};
|
|
463
712
|
}
|