@navios/react-query 0.5.1 → 0.6.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.
@@ -10,6 +10,7 @@ import { makeMutation } from '../mutation/make-hook.mjs'
10
10
 
11
11
  vi.mock('@tanstack/react-query', async (importReal) => {
12
12
  const actual = await importReal<typeof import('@tanstack/react-query')>()
13
+ const mockMutationContext = { mutationId: 1, meta: undefined }
13
14
  return {
14
15
  ...actual,
15
16
  useQueryClient: () => ({
@@ -22,11 +23,27 @@ vi.mock('@tanstack/react-query', async (importReal) => {
22
23
  ...req,
23
24
  mutateAsync: async (data: unknown) => {
24
25
  try {
26
+ const onMutateResult = await req.onMutate?.(data, mockMutationContext)
25
27
  const res = await req.mutationFn(data)
26
- await req.onSuccess?.(res, data)
28
+ await req.onSuccess?.(res, data, onMutateResult, mockMutationContext)
29
+ await req.onSettled?.(
30
+ res,
31
+ null,
32
+ data,
33
+ onMutateResult,
34
+ mockMutationContext,
35
+ )
27
36
  return res
28
37
  } catch (err) {
29
- req.onError?.(err, data)
38
+ const onMutateResult = undefined
39
+ await req.onError?.(err, data, onMutateResult, mockMutationContext)
40
+ await req.onSettled?.(
41
+ undefined,
42
+ err,
43
+ data,
44
+ onMutateResult,
45
+ mockMutationContext,
46
+ )
30
47
  throw err
31
48
  }
32
49
  },
@@ -77,7 +94,7 @@ describe('makeMutation', () => {
77
94
  }
78
95
  return data
79
96
  },
80
- onSuccess: (queryClient, data, variables) => {
97
+ onSuccess: (data, variables, context) => {
81
98
  expect(data).toMatchObject({
82
99
  success: true,
83
100
  test: 'test',
@@ -95,10 +112,12 @@ describe('makeMutation', () => {
95
112
  foo: 'bar',
96
113
  },
97
114
  })
115
+ expect(context).toHaveProperty('mutationId')
116
+ expect(context).toHaveProperty('onMutateResult')
98
117
  },
99
118
 
100
- onError: (err) => {
101
- console.log('onError', err)
119
+ onError: (err, _variables, context) => {
120
+ console.log('onError', err, context)
102
121
  },
103
122
  })
104
123
  // @ts-expect-error internal type
@@ -137,4 +156,194 @@ describe('makeMutation', () => {
137
156
  // @ts-expect-error from mock
138
157
  expect(mutationResult.mutationKey).toMatchObject(['test', '1', 'foo', '2'])
139
158
  })
159
+
160
+ describe('stream mutations', () => {
161
+ const streamEndpoint = api.declareStream({
162
+ method: 'GET',
163
+ url: '/files/$fileId/download' as const,
164
+ })
165
+
166
+ adapter.mock('/files/123/download', 'GET', () => {
167
+ const blob = new Blob(['test file content'], { type: 'text/plain' })
168
+ return new Response(blob, {
169
+ status: 200,
170
+ statusText: 'OK',
171
+ headers: {
172
+ 'content-type': 'application/octet-stream',
173
+ },
174
+ })
175
+ })
176
+
177
+ it('should work without processResponse (returns Blob)', async () => {
178
+ // @ts-expect-error stream endpoint type differs from regular endpoint
179
+ const mutation = makeMutation(streamEndpoint, {})
180
+ // @ts-expect-error internal type
181
+ const mutationResult = mutation()
182
+
183
+ const result = await mutationResult.mutateAsync({
184
+ urlParams: { fileId: '123' },
185
+ })
186
+
187
+ expect(result).toBeInstanceOf(Blob)
188
+ })
189
+
190
+ it('should work with processResponse transformation', async () => {
191
+ // @ts-expect-error stream endpoint type differs from regular endpoint
192
+ const mutation = makeMutation(streamEndpoint, {
193
+ processResponse: (blob: Blob) => URL.createObjectURL(blob),
194
+ })
195
+ // @ts-expect-error internal type
196
+ const mutationResult = mutation()
197
+
198
+ const result = await mutationResult.mutateAsync({
199
+ urlParams: { fileId: '123' },
200
+ })
201
+
202
+ expect(typeof result).toBe('string')
203
+ expect(result).toContain('blob:')
204
+ })
205
+
206
+ it('should call onSuccess with Blob data and context', async () => {
207
+ const onSuccess = vi.fn()
208
+ // @ts-expect-error stream endpoint type differs from regular endpoint
209
+ const mutation = makeMutation(streamEndpoint, {
210
+ onSuccess,
211
+ })
212
+ // @ts-expect-error internal type
213
+ const mutationResult = mutation()
214
+
215
+ await mutationResult.mutateAsync({
216
+ urlParams: { fileId: '123' },
217
+ })
218
+
219
+ expect(onSuccess).toHaveBeenCalledTimes(1)
220
+ expect(onSuccess.mock.calls[0][0]).toBeInstanceOf(Blob)
221
+ expect(onSuccess.mock.calls[0][1]).toMatchObject({
222
+ urlParams: { fileId: '123' },
223
+ })
224
+ // Third argument should be context with onMutateResult and mutationId
225
+ expect(onSuccess.mock.calls[0][2]).toHaveProperty('onMutateResult')
226
+ expect(onSuccess.mock.calls[0][2]).toHaveProperty('mutationId')
227
+ })
228
+ })
229
+
230
+ describe('mutation callbacks', () => {
231
+ it('should call onMutate before mutation and pass result to other callbacks', async () => {
232
+ const callOrder: string[] = []
233
+ const onMutate = vi.fn((_variables, _context) => {
234
+ callOrder.push('onMutate')
235
+ return { optimisticId: 'temp-123' }
236
+ })
237
+ const onSuccess = vi.fn((_data, _variables, context) => {
238
+ callOrder.push('onSuccess')
239
+ expect(context.onMutateResult).toEqual({ optimisticId: 'temp-123' })
240
+ })
241
+ const onSettled = vi.fn((_data, _error, _variables, context) => {
242
+ callOrder.push('onSettled')
243
+ expect(context.onMutateResult).toEqual({ optimisticId: 'temp-123' })
244
+ })
245
+
246
+ const mutation = makeMutation(endpoint, {
247
+ processResponse: (data) => {
248
+ if (!data.success) throw new Error(data.message)
249
+ return data
250
+ },
251
+ onMutate,
252
+ onSuccess,
253
+ onSettled,
254
+ })
255
+
256
+ // @ts-expect-error internal type
257
+ const mutationResult = mutation()
258
+ await mutationResult.mutateAsync({
259
+ urlParams: { testId: '1', fooId: '2' },
260
+ data: { testId: '1', fooId: '2' },
261
+ params: { foo: 'bar' },
262
+ })
263
+
264
+ expect(callOrder).toEqual(['onMutate', 'onSuccess', 'onSettled'])
265
+ expect(onMutate).toHaveBeenCalledTimes(1)
266
+ expect(onSuccess).toHaveBeenCalledTimes(1)
267
+ expect(onSettled).toHaveBeenCalledTimes(1)
268
+ })
269
+
270
+ it('should call onError and onSettled on failure', async () => {
271
+ adapter.mock('/test/1/foo/2', 'POST', () => {
272
+ return new Response(
273
+ JSON.stringify({ success: false, message: 'Test error' }),
274
+ { status: 200, headers: { 'content-type': 'application/json' } },
275
+ )
276
+ })
277
+
278
+ const onError = vi.fn()
279
+ const onSettled = vi.fn()
280
+
281
+ const mutation = makeMutation(endpoint, {
282
+ processResponse: (data) => {
283
+ if (!data.success) throw new Error(data.message)
284
+ return data
285
+ },
286
+ onError,
287
+ onSettled,
288
+ })
289
+
290
+ // @ts-expect-error internal type
291
+ const mutationResult = mutation()
292
+
293
+ await expect(
294
+ mutationResult.mutateAsync({
295
+ urlParams: { testId: '1', fooId: '2' },
296
+ data: { testId: '1', fooId: '2' },
297
+ params: { foo: 'bar' },
298
+ }),
299
+ ).rejects.toThrow('Test error')
300
+
301
+ expect(onError).toHaveBeenCalledTimes(1)
302
+ expect(onError.mock.calls[0][0]).toBeInstanceOf(Error)
303
+ expect(onError.mock.calls[0][0].message).toBe('Test error')
304
+ expect(onSettled).toHaveBeenCalledTimes(1)
305
+ expect(onSettled.mock.calls[0][1]).toBeInstanceOf(Error)
306
+
307
+ // Restore the mock
308
+ adapter.mock('/test/1/foo/2', 'POST', () => {
309
+ return new Response(
310
+ JSON.stringify({ success: true, test: 'test' }),
311
+ { status: 200, headers: { 'content-type': 'application/json' } },
312
+ )
313
+ })
314
+ })
315
+
316
+ it('should merge useContext result with MutationFunctionContext', async () => {
317
+ const useContext = () => ({
318
+ queryClient: { invalidate: vi.fn() },
319
+ customValue: 'test',
320
+ })
321
+
322
+ const onSuccess = vi.fn()
323
+
324
+ const mutation = makeMutation(endpoint, {
325
+ processResponse: (data) => {
326
+ if (!data.success) throw new Error(data.message)
327
+ return data
328
+ },
329
+ useContext,
330
+ onSuccess,
331
+ })
332
+
333
+ // @ts-expect-error internal type
334
+ const mutationResult = mutation()
335
+ await mutationResult.mutateAsync({
336
+ urlParams: { testId: '1', fooId: '2' },
337
+ data: { testId: '1', fooId: '2' },
338
+ params: { foo: 'bar' },
339
+ })
340
+
341
+ expect(onSuccess).toHaveBeenCalledTimes(1)
342
+ const context = onSuccess.mock.calls[0][2]
343
+ expect(context).toHaveProperty('queryClient')
344
+ expect(context).toHaveProperty('customValue', 'test')
345
+ expect(context).toHaveProperty('mutationId')
346
+ expect(context).toHaveProperty('onMutateResult')
347
+ })
348
+ })
140
349
  })
@@ -10,11 +10,12 @@ import type { z } from 'zod/v4'
10
10
  import { assertType, describe, test } from 'vitest'
11
11
  import { z as zod } from 'zod/v4'
12
12
 
13
- import type { ClientInstance } from '../types.mjs'
13
+ import type { ClientInstance, StreamHelper } from '../types.mjs'
14
14
  import type { QueryHelpers } from '../../query/types.mjs'
15
15
  import type { MutationHelpers } from '../../mutation/types.mjs'
16
16
  import type { EndpointHelper } from '../types.mjs'
17
17
  import type { Split } from '../../common/types.mjs'
18
+ import type { BaseStreamConfig } from '@navios/builder'
18
19
 
19
20
  declare const client: ClientInstance
20
21
 
@@ -644,6 +645,155 @@ describe('ClientInstance', () => {
644
645
  })
645
646
  })
646
647
 
648
+ // Stream endpoint type declarations for testing
649
+ declare const streamEndpointGet: {
650
+ config: BaseStreamConfig<'GET', '/files/$fileId/download', undefined, undefined>
651
+ } & ((params: { urlParams: { fileId: string | number } }) => Promise<Blob>)
652
+
653
+ declare const streamEndpointGetWithQuery: {
654
+ config: BaseStreamConfig<
655
+ 'GET',
656
+ '/files/$fileId/download',
657
+ typeof querySchema,
658
+ undefined
659
+ >
660
+ } & ((params: {
661
+ urlParams: { fileId: string | number }
662
+ params: z.input<typeof querySchema>
663
+ }) => Promise<Blob>)
664
+
665
+ declare const streamEndpointPost: {
666
+ config: BaseStreamConfig<
667
+ 'POST',
668
+ '/files/$fileId/export',
669
+ undefined,
670
+ typeof requestSchema
671
+ >
672
+ } & ((params: {
673
+ urlParams: { fileId: string | number }
674
+ data: z.input<typeof requestSchema>
675
+ }) => Promise<Blob>)
676
+
677
+ describe('mutationFromEndpoint() with stream endpoints', () => {
678
+ test('GET stream mutation without options (returns Blob)', () => {
679
+ const mutation = client.mutationFromEndpoint(streamEndpointGet)
680
+
681
+ // Should return a function that returns UseMutationResult with Blob
682
+ assertType<
683
+ () => UseMutationResult<Blob, Error, { urlParams: { fileId: string | number } }>
684
+ >(mutation)
685
+
686
+ // Should have StreamHelper
687
+ assertType<
688
+ StreamHelper<'GET', '/files/$fileId/download', undefined, undefined>['endpoint']
689
+ >(mutation.endpoint)
690
+ })
691
+
692
+ test('GET stream mutation with processResponse transformation', () => {
693
+ const mutation = client.mutationFromEndpoint(streamEndpointGet, {
694
+ processResponse: (blob) => URL.createObjectURL(blob),
695
+ })
696
+
697
+ // Result type should be string (transformed)
698
+ assertType<
699
+ () => UseMutationResult<
700
+ string,
701
+ Error,
702
+ { urlParams: { fileId: string | number } }
703
+ >
704
+ >(mutation)
705
+ })
706
+
707
+ test('GET stream mutation with useKey', () => {
708
+ const mutation = client.mutationFromEndpoint(streamEndpointGet, {
709
+ useKey: true,
710
+ })
711
+
712
+ // With useKey, should require urlParams in the call
713
+ assertType<
714
+ (params: {
715
+ urlParams: { fileId: string | number }
716
+ }) => UseMutationResult<
717
+ Blob,
718
+ Error,
719
+ { urlParams: { fileId: string | number } }
720
+ >
721
+ >(mutation)
722
+
723
+ // Should have MutationHelpers
724
+ assertType<
725
+ MutationHelpers<'/files/$fileId/download', Blob>['mutationKey']
726
+ >(mutation.mutationKey)
727
+ })
728
+
729
+ test('GET stream mutation with querySchema', () => {
730
+ const mutation = client.mutationFromEndpoint(streamEndpointGetWithQuery)
731
+
732
+ assertType<
733
+ () => UseMutationResult<
734
+ Blob,
735
+ Error,
736
+ {
737
+ urlParams: { fileId: string | number }
738
+ params: z.input<typeof querySchema>
739
+ }
740
+ >
741
+ >(mutation)
742
+ })
743
+
744
+ test('POST stream mutation with requestSchema', () => {
745
+ const mutation = client.mutationFromEndpoint(streamEndpointPost)
746
+
747
+ assertType<
748
+ () => UseMutationResult<
749
+ Blob,
750
+ Error,
751
+ {
752
+ urlParams: { fileId: string | number }
753
+ data: z.input<typeof requestSchema>
754
+ }
755
+ >
756
+ >(mutation)
757
+ })
758
+
759
+ test('stream mutation with onSuccess callback', () => {
760
+ const mutation = client.mutationFromEndpoint(streamEndpointGet, {
761
+ onSuccess: (_queryClient, data, variables) => {
762
+ // data should be Blob
763
+ assertType<Blob>(data)
764
+ // variables should have urlParams
765
+ assertType<{ urlParams: { fileId: string | number } }>(variables)
766
+ },
767
+ })
768
+
769
+ assertType<
770
+ () => UseMutationResult<
771
+ Blob,
772
+ Error,
773
+ { urlParams: { fileId: string | number } }
774
+ >
775
+ >(mutation)
776
+ })
777
+
778
+ test('stream mutation with custom processResponse and onSuccess', () => {
779
+ const mutation = client.mutationFromEndpoint(streamEndpointGet, {
780
+ processResponse: (blob) => ({ url: URL.createObjectURL(blob), size: blob.size }),
781
+ onSuccess: (_queryClient, data) => {
782
+ // data should be the transformed type
783
+ assertType<{ url: string; size: number }>(data)
784
+ },
785
+ })
786
+
787
+ assertType<
788
+ () => UseMutationResult<
789
+ { url: string; size: number },
790
+ Error,
791
+ { urlParams: { fileId: string | number } }
792
+ >
793
+ >(mutation)
794
+ })
795
+ })
796
+
647
797
  describe('Error cases - should fail type checking', () => {
648
798
  test('GET query without urlParams when URL has params', () => {
649
799
  const query = client.query({
@@ -1,12 +1,21 @@
1
1
  import type {
2
2
  AbstractEndpoint,
3
+ AbstractStream,
3
4
  AnyEndpointConfig,
5
+ AnyStreamConfig,
4
6
  HttpMethod,
5
7
  } from '@navios/builder'
6
- import type { InfiniteData, QueryClient } from '@tanstack/react-query'
8
+ import type {
9
+ InfiniteData,
10
+ MutationFunctionContext,
11
+ QueryClient,
12
+ } from '@tanstack/react-query'
7
13
  import type { z, ZodObject, ZodType } from 'zod/v4'
8
14
 
9
- import type { ClientOptions, ProcessResponseFunction } from '../common/types.mjs'
15
+ import type {
16
+ ClientOptions,
17
+ ProcessResponseFunction,
18
+ } from '../common/types.mjs'
10
19
  import type { MutationArgs } from '../mutation/types.mjs'
11
20
  import type { ClientInstance } from './types.mjs'
12
21
 
@@ -82,6 +91,7 @@ export interface MutationConfig<
82
91
  Response extends ZodType = ZodType,
83
92
  ReqResult = z.output<Response>,
84
93
  Result = unknown,
94
+ TOnMutateResult = unknown,
85
95
  Context = unknown,
86
96
  UseKey extends boolean = false,
87
97
  > {
@@ -93,18 +103,30 @@ export interface MutationConfig<
93
103
  processResponse: ProcessResponseFunction<Result, ReqResult>
94
104
  useContext?: () => Context
95
105
  onSuccess?: (
96
- queryClient: QueryClient,
97
- data: NoInfer<Result>,
106
+ data: Result,
98
107
  variables: MutationArgs<Url, RequestSchema, QuerySchema>,
99
- context: Context,
108
+ context: Context &
109
+ MutationFunctionContext & { onMutateResult: TOnMutateResult | undefined },
100
110
  ) => void | Promise<void>
101
111
  onError?: (
102
- queryClient: QueryClient,
103
- error: Error,
112
+ err: unknown,
113
+ variables: MutationArgs<Url, RequestSchema, QuerySchema>,
114
+ context: Context &
115
+ MutationFunctionContext & { onMutateResult: TOnMutateResult | undefined },
116
+ ) => void | Promise<void>
117
+ onMutate?: (
118
+ variables: MutationArgs<Url, RequestSchema, QuerySchema>,
119
+ context: Context & MutationFunctionContext,
120
+ ) => TOnMutateResult | Promise<TOnMutateResult>
121
+ onSettled?: (
122
+ data: Result | undefined,
123
+ error: Error | null,
104
124
  variables: MutationArgs<Url, RequestSchema, QuerySchema>,
105
- context: Context,
125
+ context: Context &
126
+ MutationFunctionContext & { onMutateResult: TOnMutateResult | undefined },
106
127
  ) => void | Promise<void>
107
128
  useKey?: UseKey
129
+ meta?: Record<string, unknown>
108
130
  }
109
131
 
110
132
  /**
@@ -235,6 +257,7 @@ export function declareClient<Options extends ClientOptions>({
235
257
  // @ts-expect-error We forgot about the DELETE method in original makeMutation
236
258
  onError: config.onError,
237
259
  useKey: config.useKey,
260
+ meta: config.meta,
238
261
  ...defaults,
239
262
  })
240
263
 
@@ -244,9 +267,11 @@ export function declareClient<Options extends ClientOptions>({
244
267
  }
245
268
 
246
269
  function mutationFromEndpoint(
247
- endpoint: AbstractEndpoint<AnyEndpointConfig>,
248
- options: {
249
- processResponse: ProcessResponseFunction
270
+ endpoint:
271
+ | AbstractEndpoint<AnyEndpointConfig>
272
+ | AbstractStream<AnyStreamConfig>,
273
+ options?: {
274
+ processResponse?: ProcessResponseFunction
250
275
  useContext?: () => unknown
251
276
  onSuccess?: (
252
277
  queryClient: QueryClient,
@@ -262,12 +287,12 @@ export function declareClient<Options extends ClientOptions>({
262
287
  ) => void | Promise<void>
263
288
  },
264
289
  ) {
290
+ // @ts-expect-error endpoint types are compatible at runtime
265
291
  return makeMutation(endpoint, {
266
- processResponse: options.processResponse,
267
- useContext: options.useContext,
268
- onSuccess: options.onSuccess,
269
- // @ts-expect-error simplify types here
270
- onError: options.onError,
292
+ processResponse: options?.processResponse,
293
+ useContext: options?.useContext,
294
+ onSuccess: options?.onSuccess,
295
+ onError: options?.onError,
271
296
  ...defaults,
272
297
  })
273
298
  }
@@ -289,6 +314,10 @@ export function declareClient<Options extends ClientOptions>({
289
314
  onSuccess: config.onSuccess,
290
315
  // @ts-expect-error We forgot about the DELETE method in original makeMutation
291
316
  onError: config.onError,
317
+ // @ts-expect-error We forgot about the DELETE method in original makeMutation
318
+ onMutate: config.onMutate,
319
+ // @ts-expect-error We forgot about the DELETE method in original makeMutation
320
+ onSettled: config.onSettled,
292
321
  useKey: config.useKey,
293
322
  ...defaults,
294
323
  })