@navios/react-query 0.1.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.
@@ -0,0 +1,140 @@
1
+ import { create } from 'navios'
2
+ import { makeNaviosFakeAdapter } from 'navios/testing'
3
+
4
+ import { builder } from '@navios/common'
5
+
6
+ import { describe, expect, it, vi } from 'vitest'
7
+ import { z } from 'zod'
8
+
9
+ import { makeMutation } from '../make-mutation.mjs'
10
+
11
+ vi.mock('@tanstack/react-query', async (importReal) => {
12
+ const actual = await importReal<typeof import('@tanstack/react-query')>()
13
+ return {
14
+ ...actual,
15
+ useQueryClient: () => ({
16
+ getQueryData: vi.fn(),
17
+ setQueryData: vi.fn(),
18
+ invalidateQueries: vi.fn(),
19
+ removeQueries: vi.fn(),
20
+ }),
21
+ useMutation: vi.fn((req) => ({
22
+ ...req,
23
+ mutateAsync: async (data: unknown) => {
24
+ try {
25
+ const res = await req.mutationFn(data)
26
+ await req.onSuccess?.(res, data)
27
+ return res
28
+ } catch (err) {
29
+ req.onError?.(err, data)
30
+ throw err
31
+ }
32
+ },
33
+ mutate: req.mutationFn,
34
+ })),
35
+ }
36
+ })
37
+
38
+ describe('makeMutation', () => {
39
+ const adapter = makeNaviosFakeAdapter()
40
+ const api = builder({})
41
+ api.provideClient(create({ adapter: adapter.fetch }))
42
+ const responseSchema = z.discriminatedUnion('success', [
43
+ z.object({ success: z.literal(true), test: z.string() }),
44
+ z.object({ success: z.literal(false), message: z.string() }),
45
+ ])
46
+ const endpoint = api.declareEndpoint({
47
+ method: 'POST',
48
+ url: '/test/$testId/foo/$fooId' as const,
49
+ requestSchema: z.object({
50
+ testId: z.string(),
51
+ fooId: z.string(),
52
+ }),
53
+ querySchema: z.object({ foo: z.string() }),
54
+ responseSchema,
55
+ })
56
+ adapter.mock('/test/1/foo/2', 'POST', () => {
57
+ return new Response(
58
+ JSON.stringify({
59
+ success: true,
60
+ test: 'test',
61
+ }),
62
+ {
63
+ status: 200,
64
+ statusText: 'OK',
65
+ headers: {
66
+ 'content-type': 'application/json',
67
+ },
68
+ },
69
+ )
70
+ })
71
+
72
+ it('should just work', async () => {
73
+ const mutation = makeMutation(endpoint, {
74
+ processResponse: (data) => {
75
+ if (!data.success) {
76
+ throw new Error(data.message)
77
+ }
78
+ return data
79
+ },
80
+ onSuccess: (queryClient, data, variables) => {
81
+ expect(data).toMatchObject({
82
+ success: true,
83
+ test: 'test',
84
+ })
85
+ expect(variables).toMatchObject({
86
+ urlParams: {
87
+ testId: '1',
88
+ fooId: '2',
89
+ },
90
+ data: {
91
+ testId: '1',
92
+ fooId: '2',
93
+ },
94
+ params: {
95
+ foo: 'bar',
96
+ },
97
+ })
98
+ },
99
+
100
+ onError: (err) => {
101
+ console.log('onError', err)
102
+ },
103
+ })
104
+ // @ts-expect-error internal type
105
+ const mutationResult = mutation()
106
+ await mutationResult.mutateAsync({
107
+ urlParams: {
108
+ testId: '1',
109
+ fooId: '2',
110
+ },
111
+ data: {
112
+ testId: '1',
113
+ fooId: '2',
114
+ },
115
+ params: {
116
+ foo: 'bar',
117
+ },
118
+ })
119
+ })
120
+
121
+ it('should work with a key', async () => {
122
+ const mutation = makeMutation(endpoint, {
123
+ processResponse: (data) => {
124
+ if (!data.success) {
125
+ throw new Error(data.message)
126
+ }
127
+ return data
128
+ },
129
+ useKey: true,
130
+ })
131
+ const mutationResult = mutation({
132
+ fooId: '2',
133
+ testId: '1',
134
+ })
135
+ console.log('mutationResult', mutationResult)
136
+
137
+ // @ts-expect-error from mock
138
+ expect(mutationResult.mutationKey).toMatchObject(['test', '1', 'foo', '2'])
139
+ })
140
+ })
@@ -0,0 +1,70 @@
1
+ import { builder } from '@navios/common'
2
+
3
+ import { QueryClient } from '@tanstack/react-query'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { z } from 'zod'
6
+
7
+ import { makeQueryOptions } from '../make-query-options.mjs'
8
+
9
+ describe('makeDataTag', () => {
10
+ it('should return a string', () => {
11
+ const api = builder({})
12
+ const responseSchema = z.discriminatedUnion('success', [
13
+ z.object({ success: z.literal(true), test: z.string() }),
14
+ z.object({ success: z.literal(false), message: z.string() }),
15
+ ])
16
+ const endpoint = api.declareEndpoint({
17
+ method: 'GET',
18
+ url: '/test/$testId/foo/$fooId' as const,
19
+ responseSchema,
20
+ })
21
+
22
+ const result = makeQueryOptions(endpoint, {
23
+ processResponse(data) {
24
+ return data
25
+ },
26
+ })
27
+ expect(typeof result.queryKey.dataTag).toBe('function')
28
+ expect(
29
+ result.queryKey.dataTag({
30
+ urlParams: { testId: '1', fooId: 'bar' },
31
+ }),
32
+ ).toMatchInlineSnapshot(`
33
+ [
34
+ "test",
35
+ "1",
36
+ "foo",
37
+ "bar",
38
+ [],
39
+ ]
40
+ `)
41
+ })
42
+ it('should return a string with the correct format', () => {
43
+ const queryClient = new QueryClient()
44
+ const api = builder({})
45
+ const responseSchema = z.discriminatedUnion('success', [
46
+ z.object({ success: z.literal(true), test: z.string() }),
47
+ z.object({ success: z.literal(false), message: z.string() }),
48
+ ])
49
+ const endpoint = api.declareEndpoint({
50
+ method: 'GET',
51
+ url: '/test/$testId/foo/$fooId' as const,
52
+ responseSchema,
53
+ })
54
+
55
+ const result = makeQueryOptions(endpoint, {
56
+ processResponse(data) {
57
+ if (!data.success) {
58
+ throw new Error(data.message)
59
+ }
60
+ return data
61
+ },
62
+ })
63
+ expect(typeof result.queryKey.dataTag).toBe('function')
64
+ const queryKey = result.queryKey.dataTag({
65
+ urlParams: { testId: '1', fooId: 'bar' },
66
+ })
67
+
68
+ queryClient.setQueryData(queryKey, { success: true, test: 'bar' })
69
+ })
70
+ })
@@ -0,0 +1,45 @@
1
+ import { builder } from '@navios/common'
2
+
3
+ import { describe, expect, it } from 'vitest'
4
+ import { z } from 'zod'
5
+
6
+ import { makeInfiniteQueryOptions } from '../make-infinite-query-options.mjs'
7
+
8
+ describe('makeInfiniteQueryOptions', () => {
9
+ const api = builder({})
10
+ const responseSchema = z.discriminatedUnion('success', [
11
+ z.object({ success: z.literal(true), test: z.string() }),
12
+ z.object({ success: z.literal(false), message: z.string() }),
13
+ ])
14
+ const endpoint = api.declareEndpoint({
15
+ method: 'GET',
16
+ url: '/test/$testId/foo/$fooId' as const,
17
+ querySchema: z.object({ foo: z.string().optional() }),
18
+ responseSchema,
19
+ })
20
+ it('should work with types', () => {
21
+ const makeOptions = makeInfiniteQueryOptions(
22
+ endpoint,
23
+ {
24
+ getNextPageParam: (lastPage) => ({
25
+ foo: 'test' in lastPage ? lastPage.test : undefined,
26
+ }),
27
+ processResponse: (data) => {
28
+ if (!data.success) {
29
+ throw new Error(data.message)
30
+ }
31
+ return data
32
+ },
33
+ },
34
+ {
35
+ select: (data) => data.pages.map((page) => page.test).flat(),
36
+ },
37
+ )
38
+ const options = makeOptions({
39
+ // @ts-expect-error it's internal type
40
+ urlParams: { testId: '1', fooId: '2' },
41
+ params: {},
42
+ })
43
+ expect(options).toBeDefined()
44
+ })
45
+ })
@@ -0,0 +1,43 @@
1
+ import { builder } from '@navios/common'
2
+
3
+ import { describe, expect, it } from 'vitest'
4
+ import { z } from 'zod'
5
+
6
+ import { makeQueryOptions } from '../make-query-options.mjs'
7
+
8
+ describe('makeQueryOptions', () => {
9
+ const api = builder({})
10
+ const responseSchema = z.discriminatedUnion('success', [
11
+ z.object({ success: z.literal(true), test: z.string() }),
12
+ z.object({ success: z.literal(false), message: z.string() }),
13
+ ])
14
+ const endpoint = api.declareEndpoint({
15
+ method: 'GET',
16
+ url: '/test/$testId/foo/$fooId' as const,
17
+ querySchema: z.object({ foo: z.string() }),
18
+ responseSchema,
19
+ })
20
+ it('should work with types', () => {
21
+ const makeOptions = makeQueryOptions(
22
+ endpoint,
23
+ {
24
+ processResponse: (data) => {
25
+ if (!data.success) {
26
+ throw new Error(data.message)
27
+ }
28
+ return data
29
+ },
30
+ },
31
+ {
32
+ select: (data) => data.test,
33
+ },
34
+ )
35
+ const options = makeOptions({
36
+ urlParams: { testId: '1', fooId: '2' },
37
+ params: {
38
+ foo: 'bar',
39
+ },
40
+ })
41
+ expect(options).toBeDefined()
42
+ })
43
+ })
@@ -0,0 +1,215 @@
1
+ import type {
2
+ AbstractEndpoint,
3
+ AnyEndpointConfig,
4
+ HttpMethod,
5
+ Util_FlatObject,
6
+ } from '@navios/common'
7
+ import type { InfiniteData, QueryClient } from '@tanstack/react-query'
8
+ import type { AnyZodObject, z, ZodType } from 'zod'
9
+
10
+ import type { ClientOptions, ProcessResponseFunction } from './types.mjs'
11
+ import type { ClientInstance, ClientMutationArgs } from './types/index.mjs'
12
+
13
+ import { makeInfiniteQueryOptions } from './make-infinite-query-options.mjs'
14
+ import { makeMutation } from './make-mutation.mjs'
15
+ import { makeQueryOptions } from './make-query-options.mjs'
16
+
17
+ export interface ClientEndpoint<
18
+ Method = HttpMethod,
19
+ Url = string,
20
+ QuerySchema = unknown,
21
+ Response = ZodType,
22
+ > {
23
+ method: Method
24
+ url: Url
25
+ querySchema?: QuerySchema
26
+ responseSchema: Response
27
+ }
28
+
29
+ export interface ClientQueryConfig<
30
+ Method = HttpMethod,
31
+ Url = string,
32
+ QuerySchema = AnyZodObject,
33
+ Response extends ZodType = ZodType,
34
+ Result = z.output<Response>,
35
+ > extends ClientEndpoint<Method, Url, QuerySchema, Response> {
36
+ processResponse?: (data: z.output<Response>) => Result
37
+ }
38
+
39
+ export type ClientInfiniteQueryConfig<
40
+ Method = HttpMethod,
41
+ Url = string,
42
+ QuerySchema extends AnyZodObject = AnyZodObject,
43
+ Response extends ZodType = ZodType,
44
+ PageResult = z.output<Response>,
45
+ Result = InfiniteData<PageResult>,
46
+ > = Required<ClientEndpoint<Method, Url, QuerySchema, Response>> & {
47
+ processResponse?: (data: z.output<Response>) => PageResult
48
+ select?: (data: InfiniteData<PageResult>) => Result
49
+ getNextPageParam: (
50
+ lastPage: PageResult,
51
+ allPages: PageResult[],
52
+ lastPageParam: z.infer<QuerySchema> | undefined,
53
+ allPageParams: z.infer<QuerySchema>[] | undefined,
54
+ ) => z.input<QuerySchema> | undefined
55
+ getPreviousPageParam?: (
56
+ firstPage: PageResult,
57
+ allPages: PageResult[],
58
+ lastPageParam: z.infer<QuerySchema> | undefined,
59
+ allPageParams: z.infer<QuerySchema>[] | undefined,
60
+ ) => z.input<QuerySchema>
61
+ initialPageParam?: z.input<QuerySchema>
62
+ }
63
+
64
+ export interface ClientMutationDataConfig<
65
+ Method extends 'POST' | 'PUT' | 'PATCH' | 'DELETE' =
66
+ | 'POST'
67
+ | 'PUT'
68
+ | 'PATCH'
69
+ | 'DELETE',
70
+ Url extends string = string,
71
+ RequestSchema = Method extends 'DELETE' ? never : AnyZodObject,
72
+ QuerySchema = unknown,
73
+ Response extends ZodType = ZodType,
74
+ ReqResult = z.output<Response>,
75
+ Result = unknown,
76
+ Context = unknown,
77
+ UseKey extends boolean = false,
78
+ > extends ClientEndpoint<Method, Url, QuerySchema, Response> {
79
+ requestSchema?: RequestSchema
80
+ processResponse: ProcessResponseFunction<Result, ReqResult>
81
+ useContext?: () => Context
82
+ onSuccess?: (
83
+ queryClient: QueryClient,
84
+ data: NoInfer<Result>,
85
+ variables: Util_FlatObject<
86
+ ClientMutationArgs<Url, RequestSchema, QuerySchema>
87
+ >,
88
+ context: Context,
89
+ ) => void | Promise<void>
90
+ onError?: (
91
+ queryClient: QueryClient,
92
+ error: Error,
93
+ variables: Util_FlatObject<
94
+ ClientMutationArgs<Url, RequestSchema, QuerySchema>
95
+ >,
96
+ context: Context,
97
+ ) => void | Promise<void>
98
+ useKey?: UseKey
99
+ }
100
+
101
+ export function declareClient<Options extends ClientOptions>({
102
+ api,
103
+ defaults = {},
104
+ }: Options): ClientInstance {
105
+ function query(config: ClientQueryConfig) {
106
+ const endpoint = api.declareEndpoint({
107
+ // @ts-expect-error we accept only specific methods
108
+ method: config.method,
109
+ url: config.url,
110
+ querySchema: config.querySchema,
111
+ responseSchema: config.responseSchema,
112
+ })
113
+
114
+ return makeQueryOptions(endpoint, {
115
+ ...defaults,
116
+ processResponse: config.processResponse ?? ((data) => data),
117
+ })
118
+ }
119
+
120
+ function queryFromEndpoint(
121
+ endpoint: AbstractEndpoint<AnyEndpointConfig>,
122
+ options?: {
123
+ processResponse?: (
124
+ data: z.output<AnyEndpointConfig['responseSchema']>,
125
+ ) => unknown
126
+ },
127
+ ) {
128
+ return makeQueryOptions(endpoint, {
129
+ ...defaults,
130
+ processResponse: options?.processResponse ?? ((data) => data),
131
+ })
132
+ }
133
+
134
+ function infiniteQuery(config: ClientInfiniteQueryConfig) {
135
+ const endpoint = api.declareEndpoint({
136
+ // @ts-expect-error we accept only specific methods
137
+ method: config.method,
138
+ url: config.url,
139
+ querySchema: config.querySchema,
140
+ responseSchema: config.responseSchema,
141
+ })
142
+ return makeInfiniteQueryOptions(endpoint, {
143
+ ...defaults,
144
+ processResponse: config.processResponse ?? ((data) => data),
145
+ getNextPageParam: config.getNextPageParam,
146
+ getPreviousPageParam: config.getPreviousPageParam,
147
+ initialPageParam: config.initialPageParam,
148
+ })
149
+ }
150
+
151
+ function infiniteQueryFromEndpoint(
152
+ endpoint: AbstractEndpoint<AnyEndpointConfig>,
153
+ options: {
154
+ processResponse?: (
155
+ data: z.output<AnyEndpointConfig['responseSchema']>,
156
+ ) => unknown
157
+ getNextPageParam: (
158
+ lastPage: z.infer<AnyEndpointConfig['responseSchema']>,
159
+ allPages: z.infer<AnyEndpointConfig['responseSchema']>[],
160
+ lastPageParam: z.infer<AnyEndpointConfig['querySchema']> | undefined,
161
+ allPageParams: z.infer<AnyEndpointConfig['querySchema']>[] | undefined,
162
+ ) => z.input<AnyEndpointConfig['querySchema']> | undefined
163
+ getPreviousPageParam?: (
164
+ firstPage: z.infer<AnyEndpointConfig['responseSchema']>,
165
+ allPages: z.infer<AnyEndpointConfig['responseSchema']>[],
166
+ lastPageParam: z.infer<AnyEndpointConfig['querySchema']> | undefined,
167
+ allPageParams: z.infer<AnyEndpointConfig['querySchema']>[] | undefined,
168
+ ) => z.input<AnyEndpointConfig['querySchema']>
169
+ initialPageParam?: z.input<AnyEndpointConfig['querySchema']>
170
+ },
171
+ ) {
172
+ return makeInfiniteQueryOptions(endpoint, {
173
+ ...defaults,
174
+ processResponse: options?.processResponse ?? ((data) => data),
175
+ getNextPageParam: options.getNextPageParam,
176
+ getPreviousPageParam: options?.getPreviousPageParam,
177
+ initialPageParam: options?.initialPageParam,
178
+ })
179
+ }
180
+
181
+ function mutation(config: ClientMutationDataConfig) {
182
+ const endpoint = api.declareEndpoint({
183
+ // @ts-expect-error We forgot about the DELETE method in original makeMutation
184
+ method: config.method,
185
+ url: config.url,
186
+ querySchema: config.querySchema,
187
+ requestSchema: config.requestSchema,
188
+ responseSchema: config.responseSchema,
189
+ })
190
+
191
+ return makeMutation(endpoint, {
192
+ processResponse: config.processResponse ?? ((data) => data),
193
+ useContext: config.useContext,
194
+ // @ts-expect-error We forgot about the DELETE method in original makeMutation
195
+ onSuccess: config.onSuccess,
196
+ // @ts-expect-error We forgot about the DELETE method in original makeMutation
197
+ onError: config.onError,
198
+ useKey: config.useKey,
199
+ ...defaults,
200
+ })
201
+ }
202
+
203
+ return {
204
+ // @ts-expect-error We simplified types here
205
+ query,
206
+ // @ts-expect-error We simplified types here
207
+ queryFromEndpoint,
208
+ // @ts-expect-error We simplified types here
209
+ infiniteQuery,
210
+ // @ts-expect-error We simplified types here
211
+ infiniteQueryFromEndpoint,
212
+ // @ts-expect-error We simplified types here
213
+ mutation,
214
+ }
215
+ }
package/src/index.mts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './types/index.mjs'
2
+ export * from './utils/mutation-key.creator.mjs'
3
+ export * from './utils/query-key-creator.mjs'
4
+ export * from './declare-client.mjs'
5
+ export * from './make-mutation.mjs'
6
+ export * from './make-infinite-query-options.mjs'
7
+ export * from './make-query-options.mjs'
8
+ export * from './types.mjs'
@@ -0,0 +1,115 @@
1
+ import type {
2
+ AbstractEndpoint,
3
+ AnyEndpointConfig,
4
+ UrlParams,
5
+ } from '@navios/common'
6
+ import type {
7
+ InfiniteData,
8
+ QueryClient,
9
+ UseInfiniteQueryOptions,
10
+ UseSuspenseInfiniteQueryOptions,
11
+ } from '@tanstack/react-query'
12
+ import type { z } from 'zod'
13
+
14
+ import {
15
+ infiniteQueryOptions,
16
+ useInfiniteQuery,
17
+ useSuspenseInfiniteQuery,
18
+ } from '@tanstack/react-query'
19
+
20
+ import type { InfiniteQueryOptions } from './types.mjs'
21
+ import type { ClientQueryArgs } from './types/index.mjs'
22
+
23
+ import { queryKeyCreator } from './utils/query-key-creator.mjs'
24
+
25
+ export function makeInfiniteQueryOptions<
26
+ Config extends AnyEndpointConfig,
27
+ Options extends InfiniteQueryOptions<Config>,
28
+ BaseQuery extends Omit<
29
+ UseInfiniteQueryOptions<ReturnType<Options['processResponse']>, Error, any>,
30
+ | 'queryKey'
31
+ | 'queryFn'
32
+ | 'getNextPageParam'
33
+ | 'initialPageParam'
34
+ | 'placeholderData'
35
+ | 'throwOnError'
36
+ >,
37
+ >(
38
+ endpoint: AbstractEndpoint<Config>,
39
+ options: Options,
40
+ baseQuery: BaseQuery = {} as BaseQuery,
41
+ ) {
42
+ const config = endpoint.config
43
+ const queryKey = queryKeyCreator(config, options, true)
44
+
45
+ const processResponse = options.processResponse
46
+ const res = (
47
+ params: ClientQueryArgs,
48
+ ): Options['processResponse'] extends (...args: any[]) => infer Result
49
+ ? UseSuspenseInfiniteQueryOptions<
50
+ Result,
51
+ Error,
52
+ BaseQuery['select'] extends (...args: any[]) => infer T
53
+ ? T
54
+ : InfiniteData<Result>
55
+ >
56
+ : never => {
57
+ // @ts-expect-error TS2322 We know that the processResponse is defined
58
+ return infiniteQueryOptions({
59
+ // @ts-expect-error TS2322 We know the type
60
+ queryKey: queryKey.dataTag(params),
61
+ queryFn: async ({ signal, pageParam }) => {
62
+ let result
63
+ try {
64
+ result = await endpoint({
65
+ signal,
66
+ // @ts-expect-error TS2345 We bind the url params only if the url has params
67
+ urlParams: params.urlParams as z.infer<UrlParams<Config['url']>>,
68
+ params: {
69
+ ...('params' in params ? params.params : {}),
70
+ ...(pageParam as z.infer<Config['querySchema']>),
71
+ },
72
+ })
73
+ } catch (err) {
74
+ if (options.onFail) {
75
+ options.onFail(err)
76
+ }
77
+ throw err
78
+ }
79
+
80
+ return processResponse(result)
81
+ },
82
+ getNextPageParam: options.getNextPageParam,
83
+ initialPageParam:
84
+ options.initialPageParam ??
85
+ config.querySchema.parse('params' in params ? params.params : {}),
86
+ ...baseQuery,
87
+ })
88
+ }
89
+ res.queryKey = queryKey
90
+
91
+ res.use = (params: ClientQueryArgs) => {
92
+ return useInfiniteQuery(res(params))
93
+ }
94
+
95
+ res.useSuspense = (params: ClientQueryArgs) => {
96
+ return useSuspenseInfiniteQuery(res(params))
97
+ }
98
+
99
+ res.invalidate = (queryClient: QueryClient, params: ClientQueryArgs) => {
100
+ return queryClient.invalidateQueries({
101
+ // @ts-expect-error We add additional function to the result
102
+ queryKey: res.queryKey.dataTag(params),
103
+ })
104
+ }
105
+
106
+ res.invalidateAll = (queryClient: QueryClient, params: ClientQueryArgs) => {
107
+ return queryClient.invalidateQueries({
108
+ // @ts-expect-error We add additional function to the result
109
+ queryKey: res.queryKey.filterKey(params),
110
+ exact: false,
111
+ })
112
+ }
113
+
114
+ return res
115
+ }