@navios/react-query 0.5.2 → 0.6.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/CHANGELOG.md +51 -10
- package/README.md +405 -273
- package/dist/src/client/declare-client.d.mts +13 -4
- package/dist/src/client/declare-client.d.mts.map +1 -1
- package/dist/src/client/types.d.mts +337 -113
- package/dist/src/client/types.d.mts.map +1 -1
- package/dist/src/mutation/make-hook.d.mts +2 -2
- package/dist/src/mutation/make-hook.d.mts.map +1 -1
- package/dist/src/mutation/types.d.mts +12 -4
- package/dist/src/mutation/types.d.mts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/_tsup-dts-rollup.d.mts +362 -120
- package/lib/_tsup-dts-rollup.d.ts +362 -120
- package/lib/index.js +51 -27
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +52 -28
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/declare-client.spec.mts +24 -5
- package/src/__tests__/make-mutation.spec.mts +150 -8
- package/src/client/__type-tests__/client-instance.spec-d.mts +2 -2
- package/src/client/declare-client.mts +33 -8
- package/src/client/types.mts +617 -141
- package/src/mutation/make-hook.mts +96 -42
- package/src/mutation/types.mts +28 -6
package/README.md
CHANGED
|
@@ -1,376 +1,508 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @navios/react-query
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe React Query integration for Navios API client with Zod schema validation.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
- **Type Safety
|
|
8
|
-
- **Validation
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **Error Handling**: The package provides built-in error handling capabilities, allowing you to handle API errors gracefully and provide meaningful feedback to users.
|
|
7
|
+
- **Type Safety** - Full TypeScript support with Zod schema inference
|
|
8
|
+
- **Schema Validation** - Request and response validation using Zod schemas
|
|
9
|
+
- **React Query Integration** - Seamless integration with TanStack Query v5
|
|
10
|
+
- **Declarative API** - Define endpoints once, use everywhere
|
|
11
|
+
- **URL Parameters** - Built-in support for parameterized URLs (`/users/$userId`)
|
|
12
|
+
- **Optimistic Updates** - First-class support via `onMutate` callback
|
|
13
|
+
- **Stream Support** - Handle file downloads and blob responses
|
|
15
14
|
|
|
16
15
|
## Installation
|
|
17
16
|
|
|
18
17
|
```bash
|
|
19
|
-
npm install
|
|
18
|
+
npm install @navios/react-query @navios/builder navios zod @tanstack/react-query
|
|
20
19
|
```
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
## Quick Start
|
|
23
22
|
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
```typescript
|
|
24
|
+
import { builder } from '@navios/builder'
|
|
25
|
+
import { declareClient } from '@navios/react-query'
|
|
26
|
+
import { create } from 'navios'
|
|
27
|
+
import { z } from 'zod/v4'
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
// Create the API builder
|
|
30
|
+
const api = builder({})
|
|
31
|
+
api.provideClient(create({ baseURL: 'https://api.example.com' }))
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
// Create the client
|
|
34
|
+
const client = declareClient({ api })
|
|
35
|
+
```
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
## Queries
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
### Basic Query
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
const getUsers = client.query({
|
|
43
|
+
method: 'GET',
|
|
44
|
+
url: '/users',
|
|
45
|
+
responseSchema: z.array(z.object({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
name: z.string(),
|
|
48
|
+
email: z.string().email(),
|
|
49
|
+
})),
|
|
39
50
|
})
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
// In a component
|
|
53
|
+
function UsersList() {
|
|
54
|
+
const { data } = getUsers.useSuspense({})
|
|
55
|
+
return (
|
|
56
|
+
<ul>
|
|
57
|
+
{data.map(user => <li key={user.id}>{user.name}</li>)}
|
|
58
|
+
</ul>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Query with URL Parameters
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const getUser = client.query({
|
|
67
|
+
method: 'GET',
|
|
68
|
+
url: '/users/$userId',
|
|
69
|
+
responseSchema: z.object({
|
|
70
|
+
id: z.string(),
|
|
71
|
+
name: z.string(),
|
|
72
|
+
}),
|
|
43
73
|
})
|
|
44
74
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
76
|
+
const { data } = getUser.useSuspense({
|
|
77
|
+
urlParams: { userId },
|
|
78
|
+
})
|
|
79
|
+
return <h1>{data.name}</h1>
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Query with Query Parameters
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const searchUsers = client.query({
|
|
87
|
+
method: 'GET',
|
|
88
|
+
url: '/users',
|
|
89
|
+
querySchema: z.object({
|
|
90
|
+
page: z.number().default(1),
|
|
91
|
+
limit: z.number().default(10),
|
|
92
|
+
search: z.string().optional(),
|
|
93
|
+
}),
|
|
94
|
+
responseSchema: z.object({
|
|
95
|
+
users: z.array(UserSchema),
|
|
96
|
+
total: z.number(),
|
|
97
|
+
}),
|
|
48
98
|
})
|
|
49
99
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
100
|
+
function UsersPage() {
|
|
101
|
+
const { data } = searchUsers.useSuspense({
|
|
102
|
+
params: { page: 1, limit: 20, search: 'john' },
|
|
103
|
+
})
|
|
104
|
+
return <div>Found {data.total} users</div>
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Query with Response Transformation
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const getUsers = client.query({
|
|
112
|
+
method: 'GET',
|
|
113
|
+
url: '/users',
|
|
55
114
|
responseSchema: z.discriminatedUnion('success', [
|
|
56
115
|
z.object({
|
|
57
116
|
success: z.literal(true),
|
|
58
|
-
data: z.
|
|
59
|
-
accessToken: z.string(),
|
|
60
|
-
refreshToken: z.string(),
|
|
61
|
-
}),
|
|
117
|
+
data: z.array(UserSchema),
|
|
62
118
|
}),
|
|
63
119
|
z.object({
|
|
64
120
|
success: z.literal(false),
|
|
65
|
-
error: z.object({
|
|
66
|
-
message: z.string(),
|
|
67
|
-
}),
|
|
121
|
+
error: z.object({ message: z.string() }),
|
|
68
122
|
}),
|
|
69
123
|
]),
|
|
70
124
|
processResponse: (response) => {
|
|
71
|
-
if (response.success) {
|
|
72
|
-
return response.data
|
|
73
|
-
} else {
|
|
125
|
+
if (!response.success) {
|
|
74
126
|
throw new Error(response.error.message)
|
|
75
127
|
}
|
|
128
|
+
return response.data
|
|
76
129
|
},
|
|
77
130
|
})
|
|
131
|
+
```
|
|
78
132
|
|
|
79
|
-
|
|
80
|
-
const { mutateAsync: login, data, isSuccess, error } = loginMutation()
|
|
133
|
+
### Query Helpers
|
|
81
134
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
email: '',
|
|
86
|
-
password: '',
|
|
87
|
-
},
|
|
88
|
-
})
|
|
135
|
+
```typescript
|
|
136
|
+
// Get query options for use with useQuery
|
|
137
|
+
const options = getUsers({ params: { page: 1 } })
|
|
89
138
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
139
|
+
// Helper hooks
|
|
140
|
+
const { data } = getUsers.use({ params: { page: 1 } })
|
|
141
|
+
const { data } = getUsers.useSuspense({ params: { page: 1 } })
|
|
142
|
+
|
|
143
|
+
// Invalidation
|
|
144
|
+
getUsers.invalidate({ params: { page: 1 } })
|
|
145
|
+
getUsers.invalidateAll({}) // Invalidate all pages
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Infinite Queries
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const getUsers = client.infiniteQuery({
|
|
152
|
+
method: 'GET',
|
|
153
|
+
url: '/users',
|
|
154
|
+
querySchema: z.object({
|
|
155
|
+
cursor: z.string().optional(),
|
|
156
|
+
limit: z.number().default(20),
|
|
157
|
+
}),
|
|
158
|
+
responseSchema: z.object({
|
|
159
|
+
users: z.array(UserSchema),
|
|
160
|
+
nextCursor: z.string().nullable(),
|
|
161
|
+
}),
|
|
162
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
function InfiniteUsersList() {
|
|
166
|
+
const { data, fetchNextPage, hasNextPage } = getUsers.useSuspense({
|
|
167
|
+
params: { limit: 20 },
|
|
168
|
+
})
|
|
95
169
|
|
|
96
170
|
return (
|
|
97
|
-
<
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<button type="submit">Login</button>
|
|
106
|
-
</form>
|
|
171
|
+
<div>
|
|
172
|
+
{data.pages.flatMap(page =>
|
|
173
|
+
page.users.map(user => <UserCard key={user.id} user={user} />)
|
|
174
|
+
)}
|
|
175
|
+
{hasNextPage && (
|
|
176
|
+
<button onClick={() => fetchNextPage()}>Load More</button>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
107
179
|
)
|
|
108
180
|
}
|
|
109
181
|
```
|
|
110
182
|
|
|
111
|
-
##
|
|
183
|
+
## Mutations
|
|
112
184
|
|
|
113
|
-
|
|
114
|
-
import { createAPI } from '@navios/navios-zod/v4'
|
|
115
|
-
import { declareClient } from '@navios/navios-zod-react'
|
|
116
|
-
|
|
117
|
-
import { z } from 'zod/v4'
|
|
185
|
+
### Basic Mutation
|
|
118
186
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
187
|
+
```typescript
|
|
188
|
+
const createUser = client.mutation({
|
|
189
|
+
method: 'POST',
|
|
190
|
+
url: '/users',
|
|
191
|
+
requestSchema: z.object({
|
|
192
|
+
name: z.string(),
|
|
193
|
+
email: z.string().email(),
|
|
194
|
+
}),
|
|
195
|
+
responseSchema: z.object({
|
|
196
|
+
id: z.string(),
|
|
197
|
+
name: z.string(),
|
|
198
|
+
}),
|
|
199
|
+
processResponse: (data) => data,
|
|
122
200
|
})
|
|
123
201
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
})
|
|
202
|
+
function CreateUserForm() {
|
|
203
|
+
const { mutateAsync, isPending } = createUser()
|
|
127
204
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
205
|
+
const handleSubmit = async (formData: FormData) => {
|
|
206
|
+
await mutateAsync({
|
|
207
|
+
data: {
|
|
208
|
+
name: formData.get('name') as string,
|
|
209
|
+
email: formData.get('email') as string,
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return <form onSubmit={handleSubmit}>...</form>
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Mutation with URL Parameters
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
const updateUser = client.mutation({
|
|
222
|
+
method: 'PUT',
|
|
223
|
+
url: '/users/$userId',
|
|
224
|
+
requestSchema: z.object({
|
|
225
|
+
name: z.string(),
|
|
134
226
|
}),
|
|
135
|
-
responseSchema:
|
|
136
|
-
|
|
137
|
-
success: z.literal(true),
|
|
138
|
-
data: z.array(
|
|
139
|
-
z.object({
|
|
140
|
-
id: z.string(),
|
|
141
|
-
name: z.string(),
|
|
142
|
-
email: z.string().email(),
|
|
143
|
-
}),
|
|
144
|
-
),
|
|
145
|
-
}),
|
|
146
|
-
z.object({
|
|
147
|
-
success: z.literal(false),
|
|
148
|
-
error: z.object({
|
|
149
|
-
message: z.string(),
|
|
150
|
-
}),
|
|
151
|
-
}),
|
|
152
|
-
]),
|
|
153
|
-
processResponse: (response) => {
|
|
154
|
-
if (response.success) {
|
|
155
|
-
return response.data
|
|
156
|
-
} else {
|
|
157
|
-
throw new Error(response.error.message)
|
|
158
|
-
}
|
|
159
|
-
},
|
|
227
|
+
responseSchema: UserSchema,
|
|
228
|
+
processResponse: (data) => data,
|
|
160
229
|
})
|
|
161
230
|
|
|
162
|
-
|
|
163
|
-
const {
|
|
164
|
-
const navigate = routeApi.useNavigate()
|
|
165
|
-
const { data, isLoading, error } = usersList.use({
|
|
166
|
-
params: {
|
|
167
|
-
page,
|
|
168
|
-
limit,
|
|
169
|
-
},
|
|
170
|
-
})
|
|
231
|
+
function EditUserForm({ userId }: { userId: string }) {
|
|
232
|
+
const { mutateAsync } = updateUser()
|
|
171
233
|
|
|
172
|
-
|
|
173
|
-
|
|
234
|
+
const handleSubmit = async (name: string) => {
|
|
235
|
+
await mutateAsync({
|
|
236
|
+
urlParams: { userId },
|
|
237
|
+
data: { name },
|
|
238
|
+
})
|
|
174
239
|
}
|
|
175
240
|
|
|
176
|
-
|
|
177
|
-
return <p>{error.message}</p>
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return (
|
|
181
|
-
<ul>
|
|
182
|
-
{data?.map((user) => (
|
|
183
|
-
<li key={user.id}>{user.name}</li>
|
|
184
|
-
))}
|
|
185
|
-
</ul>
|
|
186
|
-
)
|
|
241
|
+
return <form>...</form>
|
|
187
242
|
}
|
|
188
243
|
```
|
|
189
244
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
```tsx
|
|
193
|
-
import { createAPI } from '@navios/navios-zod/v4'
|
|
194
|
-
import { declareClient } from '@navios/navios-zod-react'
|
|
245
|
+
### Mutation with Callbacks and Optimistic Updates
|
|
195
246
|
|
|
196
|
-
|
|
247
|
+
```typescript
|
|
248
|
+
const updateUser = client.mutation({
|
|
249
|
+
method: 'PUT',
|
|
250
|
+
url: '/users/$userId',
|
|
251
|
+
requestSchema: z.object({ name: z.string() }),
|
|
252
|
+
responseSchema: UserSchema,
|
|
253
|
+
processResponse: (data) => data,
|
|
197
254
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
255
|
+
// Provide context (e.g., queryClient) via hook
|
|
256
|
+
useContext: () => {
|
|
257
|
+
const queryClient = useQueryClient()
|
|
258
|
+
return { queryClient }
|
|
259
|
+
},
|
|
202
260
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
261
|
+
// Called before mutation - return value available as context.onMutateResult
|
|
262
|
+
onMutate: async (variables, context) => {
|
|
263
|
+
// Cancel outgoing queries
|
|
264
|
+
await context.queryClient.cancelQueries({
|
|
265
|
+
queryKey: ['users', variables.urlParams.userId]
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// Snapshot previous value
|
|
269
|
+
const previousUser = context.queryClient.getQueryData(
|
|
270
|
+
['users', variables.urlParams.userId]
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
// Optimistically update
|
|
274
|
+
context.queryClient.setQueryData(
|
|
275
|
+
['users', variables.urlParams.userId],
|
|
276
|
+
{ ...previousUser, name: variables.data.name }
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return { previousUser }
|
|
280
|
+
},
|
|
206
281
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
querySchema: z.object({
|
|
211
|
-
page: z.number().optional().default(1),
|
|
212
|
-
limit: z.number().optional().default(10),
|
|
213
|
-
}),
|
|
214
|
-
responseSchema: z.discriminatedUnion('success', [
|
|
215
|
-
z.object({
|
|
216
|
-
success: z.literal(true),
|
|
217
|
-
data: z.array(
|
|
218
|
-
z.object({
|
|
219
|
-
id: z.string(),
|
|
220
|
-
name: z.string(),
|
|
221
|
-
email: z.string().email(),
|
|
222
|
-
}),
|
|
223
|
-
),
|
|
224
|
-
meta: z.object({
|
|
225
|
-
total: z.number(),
|
|
226
|
-
totalPages: z.number(),
|
|
227
|
-
page: z.number(),
|
|
228
|
-
}),
|
|
229
|
-
}),
|
|
230
|
-
z.object({
|
|
231
|
-
success: z.literal(false),
|
|
232
|
-
error: z.object({
|
|
233
|
-
message: z.string(),
|
|
234
|
-
}),
|
|
235
|
-
}),
|
|
236
|
-
]),
|
|
237
|
-
processResponse: (response) => {
|
|
238
|
-
if (response.success) {
|
|
239
|
-
return response.data
|
|
240
|
-
} else {
|
|
241
|
-
throw new Error(response.error.message)
|
|
242
|
-
}
|
|
282
|
+
// Called on success
|
|
283
|
+
onSuccess: (data, variables, context) => {
|
|
284
|
+
context.queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
243
285
|
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
286
|
+
|
|
287
|
+
// Called on error - rollback optimistic update
|
|
288
|
+
onError: (error, variables, context) => {
|
|
289
|
+
if (context.onMutateResult?.previousUser) {
|
|
290
|
+
context.queryClient.setQueryData(
|
|
291
|
+
['users', variables.urlParams.userId],
|
|
292
|
+
context.onMutateResult.previousUser
|
|
293
|
+
)
|
|
247
294
|
}
|
|
248
|
-
return undefined
|
|
249
295
|
},
|
|
250
|
-
|
|
251
|
-
|
|
296
|
+
|
|
297
|
+
// Called on both success and error
|
|
298
|
+
onSettled: (data, error, variables, context) => {
|
|
299
|
+
context.queryClient.invalidateQueries({
|
|
300
|
+
queryKey: ['users', variables.urlParams.userId]
|
|
301
|
+
})
|
|
252
302
|
},
|
|
253
303
|
})
|
|
304
|
+
```
|
|
254
305
|
|
|
255
|
-
|
|
256
|
-
const { page, limit } = routeApi.useSearch()
|
|
257
|
-
const { data, isLoading, error, fetchNextPage, hasNextPage } = usersList.use({
|
|
258
|
-
params: {
|
|
259
|
-
page,
|
|
260
|
-
limit,
|
|
261
|
-
},
|
|
262
|
-
})
|
|
306
|
+
### DELETE Mutation
|
|
263
307
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
308
|
+
```typescript
|
|
309
|
+
const deleteUser = client.mutation({
|
|
310
|
+
method: 'DELETE',
|
|
311
|
+
url: '/users/$userId',
|
|
312
|
+
responseSchema: z.object({ success: z.boolean() }),
|
|
313
|
+
processResponse: (data) => data,
|
|
314
|
+
})
|
|
267
315
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
316
|
+
const { mutateAsync } = deleteUser()
|
|
317
|
+
await mutateAsync({ urlParams: { userId: '123' } })
|
|
318
|
+
```
|
|
271
319
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
}
|
|
320
|
+
### Mutation with useKey (Scoped Mutations)
|
|
321
|
+
|
|
322
|
+
When `useKey` is true, mutations are scoped by URL parameters, preventing parallel mutations to the same resource:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
const updateUser = client.mutation({
|
|
326
|
+
method: 'PUT',
|
|
327
|
+
url: '/users/$userId',
|
|
328
|
+
useKey: true,
|
|
329
|
+
requestSchema: UpdateUserSchema,
|
|
330
|
+
responseSchema: UserSchema,
|
|
331
|
+
processResponse: (data) => data,
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// With useKey, you must pass urlParams when calling the hook
|
|
335
|
+
const { mutateAsync, isPending } = updateUser({
|
|
336
|
+
urlParams: { userId: '123' }
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// Check if any mutation for this user is in progress
|
|
340
|
+
const isMutating = updateUser.useIsMutating({ userId: '123' })
|
|
285
341
|
```
|
|
286
342
|
|
|
287
|
-
##
|
|
343
|
+
## Stream Endpoints (File Downloads)
|
|
288
344
|
|
|
289
|
-
|
|
345
|
+
```typescript
|
|
346
|
+
// Define stream endpoint
|
|
347
|
+
const downloadFile = api.declareStream({
|
|
348
|
+
method: 'GET',
|
|
349
|
+
url: '/files/$fileId/download',
|
|
350
|
+
})
|
|
290
351
|
|
|
291
|
-
|
|
352
|
+
// Create mutation from stream endpoint
|
|
353
|
+
const useDownloadFile = client.mutationFromEndpoint(downloadFile, {
|
|
354
|
+
// processResponse is optional - defaults to returning Blob
|
|
355
|
+
processResponse: (blob) => URL.createObjectURL(blob),
|
|
292
356
|
|
|
293
|
-
|
|
357
|
+
onSuccess: (url, variables, context) => {
|
|
358
|
+
window.open(url)
|
|
359
|
+
},
|
|
360
|
+
})
|
|
294
361
|
|
|
295
|
-
|
|
362
|
+
function DownloadButton({ fileId }: { fileId: string }) {
|
|
363
|
+
const { mutate, isPending } = useDownloadFile()
|
|
296
364
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
365
|
+
return (
|
|
366
|
+
<button
|
|
367
|
+
onClick={() => mutate({ urlParams: { fileId } })}
|
|
368
|
+
disabled={isPending}
|
|
369
|
+
>
|
|
370
|
+
{isPending ? 'Downloading...' : 'Download'}
|
|
371
|
+
</button>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
```
|
|
300
375
|
|
|
301
|
-
|
|
376
|
+
## Using Existing Endpoints
|
|
302
377
|
|
|
303
|
-
|
|
378
|
+
If you have endpoints defined separately, you can use them with the client:
|
|
304
379
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
380
|
+
```typescript
|
|
381
|
+
// Define endpoint separately
|
|
382
|
+
const getUserEndpoint = api.declareEndpoint({
|
|
383
|
+
method: 'GET',
|
|
384
|
+
url: '/users/$userId',
|
|
385
|
+
responseSchema: UserSchema,
|
|
386
|
+
})
|
|
310
387
|
|
|
311
|
-
|
|
388
|
+
// Create query from endpoint
|
|
389
|
+
const getUser = client.queryFromEndpoint(getUserEndpoint, {
|
|
390
|
+
processResponse: (data) => data,
|
|
391
|
+
})
|
|
312
392
|
|
|
313
|
-
|
|
314
|
-
|
|
393
|
+
// Create mutation from endpoint
|
|
394
|
+
const updateUser = client.mutationFromEndpoint(updateUserEndpoint, {
|
|
395
|
+
processResponse: (data) => data,
|
|
396
|
+
onSuccess: (data, variables, context) => {
|
|
397
|
+
console.log('Updated:', data)
|
|
398
|
+
},
|
|
399
|
+
})
|
|
400
|
+
```
|
|
315
401
|
|
|
316
|
-
|
|
402
|
+
## Multipart Mutations (File Uploads)
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
const uploadAvatar = client.multipartMutation({
|
|
406
|
+
method: 'POST',
|
|
407
|
+
url: '/users/$userId/avatar',
|
|
408
|
+
requestSchema: z.object({
|
|
409
|
+
file: z.instanceof(File),
|
|
410
|
+
}),
|
|
411
|
+
responseSchema: z.object({
|
|
412
|
+
url: z.string(),
|
|
413
|
+
}),
|
|
414
|
+
processResponse: (data) => data,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
function AvatarUpload({ userId }: { userId: string }) {
|
|
418
|
+
const { mutateAsync } = uploadAvatar()
|
|
419
|
+
|
|
420
|
+
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
421
|
+
const file = e.target.files?.[0]
|
|
422
|
+
if (file) {
|
|
423
|
+
const result = await mutateAsync({
|
|
424
|
+
urlParams: { userId },
|
|
425
|
+
data: { file },
|
|
426
|
+
})
|
|
427
|
+
console.log('Uploaded to:', result.url)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return <input type="file" onChange={handleFileChange} />
|
|
432
|
+
}
|
|
433
|
+
```
|
|
317
434
|
|
|
318
|
-
|
|
435
|
+
## API Reference
|
|
319
436
|
|
|
320
|
-
|
|
437
|
+
### `declareClient(options)`
|
|
321
438
|
|
|
322
|
-
|
|
439
|
+
Creates a client instance for making type-safe queries and mutations.
|
|
323
440
|
|
|
324
|
-
|
|
441
|
+
**Options:**
|
|
442
|
+
- `api` - The API builder created with `@navios/builder`
|
|
443
|
+
- `defaults` - Default options applied to all queries/mutations
|
|
325
444
|
|
|
326
|
-
|
|
445
|
+
**Returns:** `ClientInstance` with the following methods:
|
|
327
446
|
|
|
328
|
-
|
|
447
|
+
### Query Methods
|
|
329
448
|
|
|
330
|
-
- `
|
|
331
|
-
- `
|
|
449
|
+
- `client.query(config)` - Create a query
|
|
450
|
+
- `client.queryFromEndpoint(endpoint, options)` - Create a query from an existing endpoint
|
|
451
|
+
- `client.infiniteQuery(config)` - Create an infinite query
|
|
452
|
+
- `client.infiniteQueryFromEndpoint(endpoint, options)` - Create an infinite query from an endpoint
|
|
332
453
|
|
|
333
|
-
|
|
454
|
+
### Mutation Methods
|
|
334
455
|
|
|
335
|
-
|
|
456
|
+
- `client.mutation(config)` - Create a mutation
|
|
457
|
+
- `client.mutationFromEndpoint(endpoint, options)` - Create a mutation from an existing endpoint
|
|
458
|
+
- `client.multipartMutation(config)` - Create a multipart/form-data mutation
|
|
336
459
|
|
|
337
|
-
|
|
460
|
+
### Query Config
|
|
338
461
|
|
|
339
|
-
|
|
340
|
-
|
|
462
|
+
| Property | Type | Required | Description |
|
|
463
|
+
|----------|------|----------|-------------|
|
|
464
|
+
| `method` | `'GET' \| 'POST' \| 'HEAD' \| 'OPTIONS'` | Yes | HTTP method |
|
|
465
|
+
| `url` | `string` | Yes | URL pattern (e.g., `/users/$userId`) |
|
|
466
|
+
| `responseSchema` | `ZodSchema` | Yes | Zod schema for response validation |
|
|
467
|
+
| `querySchema` | `ZodObject` | No | Zod schema for query parameters |
|
|
468
|
+
| `requestSchema` | `ZodSchema` | No | Zod schema for request body (POST queries) |
|
|
469
|
+
| `processResponse` | `(data) => Result` | No | Transform the response |
|
|
341
470
|
|
|
342
|
-
|
|
471
|
+
### Mutation Config
|
|
343
472
|
|
|
344
|
-
|
|
473
|
+
| Property | Type | Required | Description |
|
|
474
|
+
|----------|------|----------|-------------|
|
|
475
|
+
| `method` | `'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | Yes | HTTP method |
|
|
476
|
+
| `url` | `string` | Yes | URL pattern (e.g., `/users/$userId`) |
|
|
477
|
+
| `responseSchema` | `ZodSchema` | Yes | Zod schema for response validation |
|
|
478
|
+
| `requestSchema` | `ZodSchema` | No | Zod schema for request body |
|
|
479
|
+
| `querySchema` | `ZodObject` | No | Zod schema for query parameters |
|
|
480
|
+
| `processResponse` | `(data) => Result` | No | Transform the response |
|
|
481
|
+
| `useKey` | `boolean` | No | Enable mutation key for scoping |
|
|
482
|
+
| `useContext` | `() => Context` | No | Hook to provide context to callbacks |
|
|
483
|
+
| `onMutate` | `(variables, context) => onMutateResult` | No | Called before mutation |
|
|
484
|
+
| `onSuccess` | `(data, variables, context) => void` | No | Called on success |
|
|
485
|
+
| `onError` | `(error, variables, context) => void` | No | Called on error |
|
|
486
|
+
| `onSettled` | `(data, error, variables, context) => void` | No | Called on completion |
|
|
345
487
|
|
|
346
|
-
|
|
488
|
+
### Context Object
|
|
347
489
|
|
|
348
|
-
|
|
349
|
-
- `method`: The HTTP method to use (PATCH, POST, PUT, DELETE, etc.).
|
|
350
|
-
- `requestSchema`: A Zod schema for validating the request body.
|
|
351
|
-
- `responseSchema`: A Zod schema for validating the response data.
|
|
352
|
-
- `processResponse`: A function that takes the response data and returns the processed data. (optional, but recommended)
|
|
353
|
-
- `useContext`: A function that is called before the mutation is executed. It can be used to set the context for the onSuccess and onError. (optional)
|
|
354
|
-
- `onSuccess`: A function that is called when the mutation is successful. (optional)
|
|
355
|
-
- `onError`: A function that is called when the mutation fails. (optional)
|
|
356
|
-
- `useKey`: If true, the mutation will have a mutation key which can be used to get the mutation status, limit parallel requests, etc. (optional, defaults to false)
|
|
490
|
+
The context passed to mutation callbacks includes:
|
|
357
491
|
|
|
358
|
-
|
|
492
|
+
- Properties from `useContext()` return value
|
|
493
|
+
- `mutationId` - TanStack Query mutation ID
|
|
494
|
+
- `meta` - Mutation metadata
|
|
495
|
+
- `onMutateResult` - Return value from `onMutate` (in `onSuccess`, `onError`, `onSettled`)
|
|
359
496
|
|
|
360
|
-
|
|
497
|
+
## Migration from 0.5.x
|
|
361
498
|
|
|
362
|
-
|
|
499
|
+
See [CHANGELOG.md](./CHANGELOG.md) for migration guide from 0.5.x to 0.6.0.
|
|
363
500
|
|
|
364
|
-
|
|
501
|
+
Key changes:
|
|
502
|
+
- Mutation callbacks now receive `(data, variables, context)` instead of `(queryClient, data, variables)`
|
|
503
|
+
- Use `useContext` hook to provide `queryClient` and other dependencies
|
|
504
|
+
- New `onMutate` and `onSettled` callbacks for optimistic updates
|
|
365
505
|
|
|
366
|
-
|
|
367
|
-
- `method`: The HTTP method to use (GET, POST, PUT, DELETE, etc.).
|
|
368
|
-
- `querySchema`: A Zod schema for validating the query parameters. (required)
|
|
369
|
-
- `responseSchema`: A Zod schema for validating the response data.
|
|
370
|
-
- `processResponse`: A function that takes the response data and returns the processed data. (optional, but recommended)
|
|
371
|
-
- `getNextPageParam`: A function that takes the last page and all pages and returns the next page param. (required)
|
|
372
|
-
- `initialPageData`: The initial data to use for the first page. (optional)
|
|
373
|
-
- `getPreviousPageParam`: A function that takes the first page and all pages and returns the previous page param. (optional)
|
|
374
|
-
- `select`: A function that takes the data and returns the selected data. (optional)
|
|
506
|
+
## License
|
|
375
507
|
|
|
376
|
-
|
|
508
|
+
MIT
|