@ic-reactor/react 3.0.0-beta.7 → 3.0.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/README.md +11 -10
- package/dist/createActorHooks.d.ts +2 -0
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js +2 -0
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +4 -0
- package/dist/createMutation.js.map +1 -1
- package/dist/hooks/index.d.ts +18 -5
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +15 -5
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts +13 -11
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts +105 -0
- package/dist/hooks/useActorMethod.d.ts.map +1 -0
- package/dist/hooks/useActorMethod.js +192 -0
- package/dist/hooks/useActorMethod.js.map +1 -0
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +13 -10
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/package.json +9 -8
- package/src/createActorHooks.ts +146 -0
- package/src/createAuthHooks.ts +137 -0
- package/src/createInfiniteQuery.ts +471 -0
- package/src/createMutation.ts +163 -0
- package/src/createQuery.ts +197 -0
- package/src/createSuspenseInfiniteQuery.ts +478 -0
- package/src/createSuspenseQuery.ts +215 -0
- package/src/hooks/index.ts +93 -0
- package/src/hooks/useActorInfiniteQuery.test.tsx +457 -0
- package/src/hooks/useActorInfiniteQuery.ts +134 -0
- package/src/hooks/useActorMethod.test.tsx +798 -0
- package/src/hooks/useActorMethod.ts +397 -0
- package/src/hooks/useActorMutation.test.tsx +220 -0
- package/src/hooks/useActorMutation.ts +124 -0
- package/src/hooks/useActorQuery.test.tsx +287 -0
- package/src/hooks/useActorQuery.ts +110 -0
- package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +472 -0
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +137 -0
- package/src/hooks/useActorSuspenseQuery.test.tsx +254 -0
- package/src/hooks/useActorSuspenseQuery.ts +112 -0
- package/src/index.ts +21 -0
- package/src/types.ts +435 -0
- package/src/validation.ts +202 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for query factories (createActorQuery, createActorSuspenseQuery, etc.)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
FunctionName,
|
|
7
|
+
ReactorReturnOk,
|
|
8
|
+
ReactorReturnErr,
|
|
9
|
+
ReactorArgs,
|
|
10
|
+
BaseActor,
|
|
11
|
+
TransformKey,
|
|
12
|
+
TransformReturnRegistry,
|
|
13
|
+
ErrResult,
|
|
14
|
+
ActorMethodReturnType,
|
|
15
|
+
} from "@ic-reactor/core"
|
|
16
|
+
import { CanisterError } from "@ic-reactor/core"
|
|
17
|
+
import { CallConfig } from "@icp-sdk/core/agent"
|
|
18
|
+
import {
|
|
19
|
+
QueryKey,
|
|
20
|
+
QueryObserverOptions,
|
|
21
|
+
UseQueryOptions,
|
|
22
|
+
UseQueryResult,
|
|
23
|
+
UseSuspenseQueryOptions,
|
|
24
|
+
UseSuspenseQueryResult,
|
|
25
|
+
UseMutationOptions,
|
|
26
|
+
UseMutationResult,
|
|
27
|
+
} from "@tanstack/react-query"
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Utility Types
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
// NoInfer prevents TypeScript from inferring a type parameter from a particular position
|
|
34
|
+
// This is available in TypeScript 5.4+ natively, but we define it for compatibility
|
|
35
|
+
export type NoInfer<T> = [T][T extends any ? 0 : never]
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Base Query Data Types
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/** The raw data type returned by the query function (before select) */
|
|
41
|
+
export type QueryFnData<
|
|
42
|
+
A = BaseActor,
|
|
43
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
44
|
+
T extends TransformKey = "candid",
|
|
45
|
+
> = ReactorReturnOk<A, M, T>
|
|
46
|
+
|
|
47
|
+
/** The error type for queries */
|
|
48
|
+
export type QueryError<
|
|
49
|
+
A = BaseActor,
|
|
50
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
51
|
+
T extends TransformKey = "candid",
|
|
52
|
+
> = ReactorReturnErr<A, M, T>
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Base Query Configuration
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Base configuration for query wrappers (shared between regular and suspense).
|
|
60
|
+
*
|
|
61
|
+
* @template A - The actor interface type
|
|
62
|
+
* @template M - The method name on the actor
|
|
63
|
+
* @template T - The transformation key (identity, display, etc.)
|
|
64
|
+
* @template TSelected - The type returned after select transformation
|
|
65
|
+
*/
|
|
66
|
+
export interface BaseQueryConfig<
|
|
67
|
+
A = BaseActor,
|
|
68
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
69
|
+
T extends TransformKey = "candid",
|
|
70
|
+
TSelected = QueryFnData<A, M, T>,
|
|
71
|
+
> extends Omit<
|
|
72
|
+
QueryObserverOptions<
|
|
73
|
+
ReactorReturnOk<A, M, T>,
|
|
74
|
+
ReactorReturnErr<A, M, T>,
|
|
75
|
+
TSelected,
|
|
76
|
+
ReactorReturnOk<A, M, T>,
|
|
77
|
+
QueryKey
|
|
78
|
+
>,
|
|
79
|
+
"queryFn" | "queryKey"
|
|
80
|
+
> {
|
|
81
|
+
/** The method to call on the canister */
|
|
82
|
+
functionName: M
|
|
83
|
+
/** Arguments to pass to the method (if any) */
|
|
84
|
+
args?: ReactorArgs<A, M, T>
|
|
85
|
+
/** The query key to use for this query */
|
|
86
|
+
queryKey?: QueryKey
|
|
87
|
+
/** How long data stays fresh before refetching (default: 5 min) */
|
|
88
|
+
staleTime?: number
|
|
89
|
+
/** Transform the raw result before returning */
|
|
90
|
+
select?: (data: QueryFnData<A, M, T>) => TSelected
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Configuration for createQuery (regular useQuery).
|
|
95
|
+
* Alias for BaseQueryConfig for clarity.
|
|
96
|
+
*/
|
|
97
|
+
export type QueryConfig<
|
|
98
|
+
A = BaseActor,
|
|
99
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
100
|
+
T extends TransformKey = "candid",
|
|
101
|
+
TSelected = QueryFnData<A, M, T>,
|
|
102
|
+
> = BaseQueryConfig<A, M, T, TSelected>
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Configuration for createSuspenseQuery (useSuspenseQuery).
|
|
106
|
+
* Alias for BaseQueryConfig for clarity.
|
|
107
|
+
*/
|
|
108
|
+
export type SuspenseQueryConfig<
|
|
109
|
+
A = BaseActor,
|
|
110
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
111
|
+
T extends TransformKey = "candid",
|
|
112
|
+
TSelected = QueryFnData<A, M, T>,
|
|
113
|
+
> = BaseQueryConfig<A, M, T, TSelected>
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Factory Configuration (without args)
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Configuration for createQueryFactory (args are provided at call time).
|
|
121
|
+
*/
|
|
122
|
+
export type QueryFactoryConfig<
|
|
123
|
+
A = BaseActor,
|
|
124
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
125
|
+
T extends TransformKey = "candid",
|
|
126
|
+
TSelected = QueryFnData<A, M, T>,
|
|
127
|
+
> = Omit<QueryConfig<A, M, T, TSelected>, "args">
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Configuration for createSuspenseQueryFactory (args are provided at call time).
|
|
131
|
+
*/
|
|
132
|
+
export type SuspenseQueryFactoryConfig<
|
|
133
|
+
A = BaseActor,
|
|
134
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
135
|
+
T extends TransformKey = "candid",
|
|
136
|
+
TSelected = QueryFnData<A, M, T>,
|
|
137
|
+
> = Omit<SuspenseQueryConfig<A, M, T, TSelected>, "args">
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Hook Interfaces with Chained Select Support
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* useQuery hook with chained select support.
|
|
145
|
+
* - Without select: returns TSelected (from config.select)
|
|
146
|
+
* - With select: chains on top and returns TFinal
|
|
147
|
+
*
|
|
148
|
+
* Accepts all useQuery options from React Query documentation.
|
|
149
|
+
* Select is special: it chains on top of config.select.
|
|
150
|
+
*/
|
|
151
|
+
export interface UseQueryWithSelect<
|
|
152
|
+
TQueryFnData,
|
|
153
|
+
TSelected = TQueryFnData,
|
|
154
|
+
TError = Error,
|
|
155
|
+
> {
|
|
156
|
+
// Overload 1: Without select - returns TSelected
|
|
157
|
+
// Note: select is included as optional (never type) to enable autocomplete suggestions
|
|
158
|
+
(
|
|
159
|
+
options?: Omit<
|
|
160
|
+
UseQueryOptions<TQueryFnData, TError, TSelected>,
|
|
161
|
+
"queryKey" | "queryFn"
|
|
162
|
+
> & {
|
|
163
|
+
select?: undefined
|
|
164
|
+
}
|
|
165
|
+
): UseQueryResult<TSelected, TError>
|
|
166
|
+
|
|
167
|
+
// Overload 2: With select - chains on top of config.select and returns TFinal
|
|
168
|
+
<TFinal = TSelected>(
|
|
169
|
+
options: Omit<
|
|
170
|
+
UseQueryOptions<TQueryFnData, TError, TFinal>,
|
|
171
|
+
"queryKey" | "queryFn" | "select"
|
|
172
|
+
> & {
|
|
173
|
+
select: (data: TSelected) => TFinal
|
|
174
|
+
}
|
|
175
|
+
): UseQueryResult<TFinal, TError>
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* useSuspenseQuery hook with chained select support.
|
|
180
|
+
* - Without select: returns TSelected (from config.select)
|
|
181
|
+
* - With select: chains on top and returns TFinal
|
|
182
|
+
*
|
|
183
|
+
* Accepts all useSuspenseQuery options from React Query documentation.
|
|
184
|
+
* Select is special: it chains on top of config.select.
|
|
185
|
+
* Data is always defined (never undefined).
|
|
186
|
+
* Does NOT support `enabled` option.
|
|
187
|
+
*/
|
|
188
|
+
export interface UseSuspenseQueryWithSelect<
|
|
189
|
+
TQueryFnData,
|
|
190
|
+
TSelected = TQueryFnData,
|
|
191
|
+
TError = Error,
|
|
192
|
+
> {
|
|
193
|
+
// Overload 1: Without select - returns TSelected
|
|
194
|
+
// Note: select is included as optional (never type) to enable autocomplete suggestions
|
|
195
|
+
(
|
|
196
|
+
options?: Omit<
|
|
197
|
+
UseSuspenseQueryOptions<TQueryFnData, TError, TSelected>,
|
|
198
|
+
"queryKey" | "queryFn"
|
|
199
|
+
> & {
|
|
200
|
+
select?: undefined
|
|
201
|
+
}
|
|
202
|
+
): UseSuspenseQueryResult<TSelected, TError>
|
|
203
|
+
|
|
204
|
+
// Overload 2: With select - chains on top of config.select and returns TFinal
|
|
205
|
+
<TFinal = TSelected>(
|
|
206
|
+
options: Omit<
|
|
207
|
+
UseSuspenseQueryOptions<TQueryFnData, TError, TFinal>,
|
|
208
|
+
"queryKey" | "queryFn" | "select"
|
|
209
|
+
> & {
|
|
210
|
+
select: (data: TSelected) => TFinal
|
|
211
|
+
}
|
|
212
|
+
): UseSuspenseQueryResult<TFinal, TError>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// Result Interfaces
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Base result interface shared between createQuery and createSuspenseQuery.
|
|
221
|
+
*
|
|
222
|
+
* @template TQueryFnData - The raw data type
|
|
223
|
+
* @template TSelected - The type after select transformation
|
|
224
|
+
* @template TError - The error type
|
|
225
|
+
*/
|
|
226
|
+
export interface BaseQueryResult<
|
|
227
|
+
TQueryFnData,
|
|
228
|
+
TSelected = TQueryFnData,
|
|
229
|
+
_TError = Error,
|
|
230
|
+
> {
|
|
231
|
+
/** Fetch data in loader (uses ensureQueryData for cache-first) */
|
|
232
|
+
fetch: () => Promise<TSelected>
|
|
233
|
+
|
|
234
|
+
/** Invalidate the cache (refetches if query is active) */
|
|
235
|
+
invalidate: () => Promise<void>
|
|
236
|
+
|
|
237
|
+
/** Get query key (for advanced React Query usage) */
|
|
238
|
+
getQueryKey: () => QueryKey
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Read data directly from cache without fetching.
|
|
242
|
+
* Returns undefined if data is not in cache.
|
|
243
|
+
*
|
|
244
|
+
* @template TFinal - Type returned after optional select transformation
|
|
245
|
+
* @param select - Optional select function to transform cached data further
|
|
246
|
+
* @returns Cached data with select applied, or undefined if not in cache
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* // Just get the cached data
|
|
250
|
+
* const user = userQuery.getCacheData()
|
|
251
|
+
*
|
|
252
|
+
* // With additional select transformation
|
|
253
|
+
* const name = userQuery.getCacheData((user) => user.name)
|
|
254
|
+
*/
|
|
255
|
+
getCacheData: {
|
|
256
|
+
(): TSelected | undefined
|
|
257
|
+
<TFinal>(select: (data: TSelected) => TFinal): TFinal | undefined
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Result from createQuery
|
|
263
|
+
*
|
|
264
|
+
* Includes useQuery hook that:
|
|
265
|
+
* - Supports `enabled` option for conditional fetching
|
|
266
|
+
* - Data may be `undefined` during loading
|
|
267
|
+
* - Uses regular `useQuery` with manual loading state handling
|
|
268
|
+
*
|
|
269
|
+
* @template TQueryFnData - The raw data type
|
|
270
|
+
* @template TSelected - The type after select transformation
|
|
271
|
+
* @template TError - The error type
|
|
272
|
+
*/
|
|
273
|
+
export interface QueryResult<
|
|
274
|
+
TQueryFnData,
|
|
275
|
+
TSelected = TQueryFnData,
|
|
276
|
+
TError = Error,
|
|
277
|
+
> extends BaseQueryResult<TQueryFnData, TSelected, TError> {
|
|
278
|
+
/** React hook for components - supports chained select and enabled option */
|
|
279
|
+
useQuery: UseQueryWithSelect<TQueryFnData, TSelected, TError>
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Result from createSuspenseQuery
|
|
284
|
+
*
|
|
285
|
+
* Includes useSuspenseQuery hook that:
|
|
286
|
+
* - Requires wrapping in <Suspense> boundary
|
|
287
|
+
* - Data is always defined (no undefined checks)
|
|
288
|
+
* - Does NOT support `enabled` option
|
|
289
|
+
*
|
|
290
|
+
* @template TQueryFnData - The raw data type
|
|
291
|
+
* @template TSelected - The type after select transformation
|
|
292
|
+
* @template TError - The error type
|
|
293
|
+
*/
|
|
294
|
+
export interface SuspenseQueryResult<
|
|
295
|
+
TQueryFnData,
|
|
296
|
+
TSelected = TQueryFnData,
|
|
297
|
+
TError = Error,
|
|
298
|
+
> extends BaseQueryResult<TQueryFnData, TSelected, TError> {
|
|
299
|
+
/** React hook for components - data is always defined (wrap in Suspense) */
|
|
300
|
+
useSuspenseQuery: UseSuspenseQueryWithSelect<TQueryFnData, TSelected, TError>
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ============================================================================
|
|
304
|
+
// Actor Mutation Types
|
|
305
|
+
// ============================================================================
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Configuration for createMutation and useActorMutation.
|
|
309
|
+
*/
|
|
310
|
+
export interface MutationConfig<
|
|
311
|
+
A = BaseActor,
|
|
312
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
313
|
+
T extends TransformKey = "candid",
|
|
314
|
+
> extends Omit<
|
|
315
|
+
UseMutationOptions<
|
|
316
|
+
ReactorReturnOk<A, M, T>,
|
|
317
|
+
ReactorReturnErr<A, M, T>,
|
|
318
|
+
ReactorArgs<A, M, T>
|
|
319
|
+
>,
|
|
320
|
+
"mutationFn"
|
|
321
|
+
> {
|
|
322
|
+
/** The method to call on the canister */
|
|
323
|
+
functionName: M
|
|
324
|
+
/** Call configuration for the actor method */
|
|
325
|
+
callConfig?: CallConfig
|
|
326
|
+
/** Queries to invalidate upon successful mutation */
|
|
327
|
+
invalidateQueries?: QueryKey[]
|
|
328
|
+
/**
|
|
329
|
+
* Callback for canister-level business logic errors.
|
|
330
|
+
* Called when the canister returns a Result { Err: E } variant.
|
|
331
|
+
*
|
|
332
|
+
* This is separate from `onError` which handles all errors including
|
|
333
|
+
* network failures, agent errors, etc.
|
|
334
|
+
*
|
|
335
|
+
* @param error - The CanisterError containing the typed error value
|
|
336
|
+
* @param variables - The arguments passed to the mutation
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* createMutation(reactor, {
|
|
341
|
+
* functionName: "transfer",
|
|
342
|
+
* onCanisterError: (error, variables) => {
|
|
343
|
+
* // error.err contains the typed Err value
|
|
344
|
+
* // error.code contains the variant key (e.g., "InsufficientFunds")
|
|
345
|
+
* console.error(`Transfer failed: ${error.code}`, error.err)
|
|
346
|
+
* },
|
|
347
|
+
* })
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
onCanisterError?: (
|
|
351
|
+
error: CanisterError<unknown>,
|
|
352
|
+
variables: ReactorArgs<A, M, T>
|
|
353
|
+
) => void
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Configuration for createMutationFactory.
|
|
358
|
+
*/
|
|
359
|
+
export type MutationFactoryConfig<
|
|
360
|
+
A = BaseActor,
|
|
361
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
362
|
+
T extends TransformKey = "candid",
|
|
363
|
+
> = Omit<MutationConfig<A, M, T>, "onSuccess">
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Options for useMutation hook.
|
|
367
|
+
* Extends React Query's UseMutationOptions with invalidateQueries support.
|
|
368
|
+
*/
|
|
369
|
+
export interface MutationHookOptions<
|
|
370
|
+
A = BaseActor,
|
|
371
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
372
|
+
T extends TransformKey = "candid",
|
|
373
|
+
> extends Omit<
|
|
374
|
+
UseMutationOptions<
|
|
375
|
+
ReactorReturnOk<A, M, T>,
|
|
376
|
+
ReactorReturnErr<A, M, T>,
|
|
377
|
+
ReactorArgs<A, M, T>
|
|
378
|
+
>,
|
|
379
|
+
"mutationFn"
|
|
380
|
+
> {
|
|
381
|
+
/**
|
|
382
|
+
* Query keys to invalidate upon successful mutation.
|
|
383
|
+
* Use query.getQueryKey() to get the key from a query result.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* const balanceQuery = getIcpBalance(account)
|
|
387
|
+
* useMutation({
|
|
388
|
+
* invalidateQueries: [balanceQuery.getQueryKey()],
|
|
389
|
+
* })
|
|
390
|
+
*/
|
|
391
|
+
invalidateQueries?: (QueryKey | undefined)[]
|
|
392
|
+
/**
|
|
393
|
+
* Callback for canister-level business logic errors.
|
|
394
|
+
* Called when the canister returns a Result { Err: E } variant.
|
|
395
|
+
*
|
|
396
|
+
* @param error - The CanisterError containing the typed error value
|
|
397
|
+
* @param variables - The arguments passed to the mutation
|
|
398
|
+
*/
|
|
399
|
+
onCanisterError?: (
|
|
400
|
+
error: CanisterError<
|
|
401
|
+
TransformReturnRegistry<ErrResult<ActorMethodReturnType<A[M]>>>[T]
|
|
402
|
+
>,
|
|
403
|
+
variables: ReactorArgs<A, M, T>
|
|
404
|
+
) => void
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Result from createMutation.
|
|
409
|
+
*/
|
|
410
|
+
export interface MutationResult<
|
|
411
|
+
A = BaseActor,
|
|
412
|
+
M extends FunctionName<A> = FunctionName<A>,
|
|
413
|
+
T extends TransformKey = "candid",
|
|
414
|
+
> {
|
|
415
|
+
/**
|
|
416
|
+
* React hook for the mutation.
|
|
417
|
+
* Accepts options to override/extend the factory config.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* // With invalidateQueries to auto-update balance after transfer
|
|
421
|
+
* const { mutate } = icpTransferMutation.useMutation({
|
|
422
|
+
* invalidateQueries: [userBalanceQuery], // Auto-invalidate after success!
|
|
423
|
+
* })
|
|
424
|
+
*/
|
|
425
|
+
useMutation: (
|
|
426
|
+
options?: MutationHookOptions<A, M, T>
|
|
427
|
+
) => UseMutationResult<
|
|
428
|
+
ReactorReturnOk<A, M, T>,
|
|
429
|
+
ReactorReturnErr<A, M, T>,
|
|
430
|
+
ReactorArgs<A, M, T>
|
|
431
|
+
>
|
|
432
|
+
|
|
433
|
+
/** Execute the update call directly (outside of React) */
|
|
434
|
+
execute: (args: ReactorArgs<A, M, T>) => Promise<ReactorReturnOk<A, M, T>>
|
|
435
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for React
|
|
3
|
+
*
|
|
4
|
+
* Helpers for working with ValidationError in React components,
|
|
5
|
+
* especially for form integration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ValidationError,
|
|
10
|
+
isValidationError,
|
|
11
|
+
ValidationIssue,
|
|
12
|
+
} from "@ic-reactor/core"
|
|
13
|
+
|
|
14
|
+
// Re-export for convenience
|
|
15
|
+
export { isValidationError, ValidationError }
|
|
16
|
+
export type { ValidationIssue }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A map of field names to error messages.
|
|
20
|
+
* This format is compatible with most form libraries.
|
|
21
|
+
*/
|
|
22
|
+
export type FieldErrors = Record<string, string>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A map of field names to arrays of error messages.
|
|
26
|
+
* Use when you need to show multiple errors per field.
|
|
27
|
+
*/
|
|
28
|
+
export type FieldErrorsMultiple = Record<string, string[]>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for mapValidationErrors
|
|
32
|
+
*/
|
|
33
|
+
export interface MapValidationErrorsOptions {
|
|
34
|
+
/**
|
|
35
|
+
* If true, returns all messages for each field as an array.
|
|
36
|
+
* If false (default), returns only the first message for each field.
|
|
37
|
+
*/
|
|
38
|
+
multiple?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maps validation error issues to a simple field -> message object.
|
|
43
|
+
* Returns the first error message for each field path.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const { mutate } = useActorMutation({
|
|
48
|
+
* functionName: "transfer",
|
|
49
|
+
* onError: (error) => {
|
|
50
|
+
* if (isValidationError(error)) {
|
|
51
|
+
* const fieldErrors = mapValidationErrors(error)
|
|
52
|
+
* // { to: "Recipient is required", amount: "Must be a number" }
|
|
53
|
+
*
|
|
54
|
+
* // Use with React Hook Form
|
|
55
|
+
* Object.entries(fieldErrors).forEach(([field, message]) => {
|
|
56
|
+
* form.setError(field, { message })
|
|
57
|
+
* })
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function mapValidationErrors(error: ValidationError): FieldErrors
|
|
64
|
+
export function mapValidationErrors(
|
|
65
|
+
error: ValidationError,
|
|
66
|
+
options: { multiple: true }
|
|
67
|
+
): FieldErrorsMultiple
|
|
68
|
+
export function mapValidationErrors(
|
|
69
|
+
error: ValidationError,
|
|
70
|
+
options: { multiple: false }
|
|
71
|
+
): FieldErrors
|
|
72
|
+
export function mapValidationErrors(
|
|
73
|
+
error: ValidationError,
|
|
74
|
+
options?: MapValidationErrorsOptions
|
|
75
|
+
): FieldErrors | FieldErrorsMultiple {
|
|
76
|
+
if (options?.multiple) {
|
|
77
|
+
const result: FieldErrorsMultiple = {}
|
|
78
|
+
for (const issue of error.issues) {
|
|
79
|
+
const fieldName = String(issue.path[0] ?? "")
|
|
80
|
+
if (!result[fieldName]) {
|
|
81
|
+
result[fieldName] = []
|
|
82
|
+
}
|
|
83
|
+
result[fieldName].push(issue.message)
|
|
84
|
+
}
|
|
85
|
+
return result
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result: FieldErrors = {}
|
|
89
|
+
for (const issue of error.issues) {
|
|
90
|
+
const fieldName = String(issue.path[0] ?? "")
|
|
91
|
+
if (!result[fieldName]) {
|
|
92
|
+
result[fieldName] = issue.message
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Gets error message for a specific field from a ValidationError.
|
|
100
|
+
* Returns undefined if no error exists for that field.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* if (isValidationError(error)) {
|
|
105
|
+
* const toError = getFieldError(error, "to")
|
|
106
|
+
* const amountError = getFieldError(error, "amount")
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export function getFieldError(
|
|
111
|
+
error: ValidationError,
|
|
112
|
+
fieldName: string
|
|
113
|
+
): string | undefined {
|
|
114
|
+
const issue = error.issues.find((i) => String(i.path[0]) === fieldName)
|
|
115
|
+
return issue?.message
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Gets all error messages for a specific field from a ValidationError.
|
|
120
|
+
* Returns empty array if no errors exist for that field.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```tsx
|
|
124
|
+
* if (isValidationError(error)) {
|
|
125
|
+
* const amountErrors = getFieldErrors(error, "amount")
|
|
126
|
+
* // ["Amount is required", "Amount must be positive"]
|
|
127
|
+
* }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function getFieldErrors(
|
|
131
|
+
error: ValidationError,
|
|
132
|
+
fieldName: string
|
|
133
|
+
): string[] {
|
|
134
|
+
return error.issues
|
|
135
|
+
.filter((i) => String(i.path[0]) === fieldName)
|
|
136
|
+
.map((i) => i.message)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Checks if a given error is a ValidationError and optionally
|
|
141
|
+
* extracts field errors in one step.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```tsx
|
|
145
|
+
* const { mutateAsync } = useActorMutation({ functionName: "transfer" })
|
|
146
|
+
*
|
|
147
|
+
* const handleSubmit = async (data) => {
|
|
148
|
+
* try {
|
|
149
|
+
* await mutateAsync([data])
|
|
150
|
+
* } catch (error) {
|
|
151
|
+
* const fieldErrors = extractValidationErrors(error)
|
|
152
|
+
* if (fieldErrors) {
|
|
153
|
+
* // Handle validation errors
|
|
154
|
+
* Object.entries(fieldErrors).forEach(([field, message]) => {
|
|
155
|
+
* form.setError(field, { message })
|
|
156
|
+
* })
|
|
157
|
+
* } else {
|
|
158
|
+
* // Handle other errors
|
|
159
|
+
* console.error(error)
|
|
160
|
+
* }
|
|
161
|
+
* }
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export function extractValidationErrors(error: unknown): FieldErrors | null {
|
|
166
|
+
if (isValidationError(error)) {
|
|
167
|
+
return mapValidationErrors(error)
|
|
168
|
+
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* React hook-friendly error handler that extracts validation errors.
|
|
174
|
+
* Returns a callback suitable for onError handlers.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```tsx
|
|
178
|
+
* const [fieldErrors, setFieldErrors] = useState<FieldErrors>({})
|
|
179
|
+
*
|
|
180
|
+
* const { mutate } = useActorMutation({
|
|
181
|
+
* functionName: "transfer",
|
|
182
|
+
* onError: handleValidationError(setFieldErrors),
|
|
183
|
+
* })
|
|
184
|
+
*
|
|
185
|
+
* // In JSX
|
|
186
|
+
* <input name="to" />
|
|
187
|
+
* {fieldErrors.to && <span className="error">{fieldErrors.to}</span>}
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export function handleValidationError(
|
|
191
|
+
setFieldErrors: (errors: FieldErrors) => void,
|
|
192
|
+
onOtherError?: (error: Error) => void
|
|
193
|
+
): (error: Error) => void {
|
|
194
|
+
return (error: Error) => {
|
|
195
|
+
const fieldErrors = extractValidationErrors(error)
|
|
196
|
+
if (fieldErrors) {
|
|
197
|
+
setFieldErrors(fieldErrors)
|
|
198
|
+
} else if (onOtherError) {
|
|
199
|
+
onOtherError(error)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|