Skip to content

Commit ebff482

Browse files
committed
improvements to tags, add asterisk tag
1 parent e8ea96f commit ebff482

File tree

13 files changed

+132
-39
lines changed

13 files changed

+132
-39
lines changed

examples/vite/src/App.tsx

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import './mocks';
2-
import { useState } from 'react';
2+
import { useRef, useState } from 'react';
33
import { CommentData, PostData, baseUrl } from './mocks';
44
import './App.css';
55
import { HttpMutationBuilder, HttpQueryBuilder, useOperateOnTags } from 'react-query-builder';
@@ -8,22 +8,45 @@ import { queryClient } from './client';
88
const baseQuery = new HttpQueryBuilder({ queryClient }).withBaseUrl(baseUrl);
99
const baseMutation = new HttpMutationBuilder({ queryClient }).withBaseUrl(baseUrl);
1010

11-
const resetMutation = baseMutation.withPath('/reset').withConfig({ invalidates: ['posts' as any, 'refreshable'] });
11+
const resetMutation = baseMutation.withPath('/reset').withConfig({ invalidates: '*' });
1212

1313
const postsQuery = baseQuery
1414
.withConfig({ tags: 'refreshable' })
1515
.withConfig({ tags: { type: 'posts' as any, id: 'LIST' } })
1616
.withPath('/posts')
1717
.withData<PostData[]>();
1818

19-
const postQuery = baseQuery.withConfig({ tags: 'refreshable' }).withPath('/posts/:id').withData<PostData>();
19+
const postQuery = baseQuery
20+
.withConfig({ tags: 'refreshable' })
21+
.withPath('/posts/:id')
22+
.withData<PostData>()
23+
.withConfig({
24+
tags: (ctx) => [{ type: 'posts' as any, id: ctx.vars.params.id }],
25+
});
2026

2127
const commentsQuery = baseQuery
2228
.withConfig({ tags: 'refreshable' })
2329
.withPath('/comments')
2430
.withSearch<{ postId: number | null }>()
2531
.withData<CommentData[]>();
2632

33+
const editPostMutation = baseMutation
34+
.withPath('/posts/:id')
35+
.withVars({ method: 'put' })
36+
.withBody<Partial<PostData>>()
37+
.withConfig({
38+
invalidates: () => [{ type: 'posts' as any, id: 'LIST' }],
39+
optimisticUpdates: (ctx) => [
40+
{
41+
type: 'posts' as any,
42+
id: ctx.vars.params.id,
43+
updater(ctx, target) {
44+
return { ...target!, ...ctx.vars.body };
45+
},
46+
},
47+
],
48+
});
49+
2750
const deletePostMutation = baseMutation
2851
.withVars({ method: 'delete' })
2952
.withPath('/posts/:id')
@@ -68,8 +91,8 @@ function App() {
6891
<h2
6992
onClick={() => setPostId(post.id)}
7093
onMouseOver={() => {
71-
postQuery.client.ensureData({ params: { id: post.id } });
72-
commentsQuery.client.ensureData({ search: { postId: post.id } });
94+
postQuery.client.prefetch({ params: { id: post.id } });
95+
commentsQuery.client.prefetch({ search: { postId: post.id } });
7396
}}
7497
>
7598
{post.title}
@@ -94,18 +117,53 @@ function PostPage({ postId, onBack }: { postId: number; onBack: () => void }) {
94117
const post = postQuery.useQuery({ params: { id: postId } });
95118
const comments = commentsQuery.useQuery({ search: { postId: postId } });
96119

120+
const [showEdit, setShowEdit] = useState(false);
121+
const editPost = editPostMutation.useMutation();
122+
123+
const titleRef = useRef<HTMLInputElement>(null);
124+
const bodyRef = useRef<HTMLTextAreaElement>(null);
125+
97126
return (
98127
<>
99-
{post.isLoading ? (
100-
'Loading...'
101-
) : post.isError ? (
102-
post.error.message
128+
{!showEdit ? (
129+
<>
130+
{post.isLoading ? (
131+
'Loading...'
132+
) : post.isError ? (
133+
post.error.message
134+
) : (
135+
<div>
136+
<h2>{post.data?.title}</h2>
137+
<p>{post.data?.body}</p>
138+
<button onClick={onBack}>Back</button>
139+
<button onClick={() => setShowEdit(true)} disabled={editPost.isPending}>
140+
Edit post
141+
</button>
142+
</div>
143+
)}
144+
</>
103145
) : (
104-
<div>
105-
<h2>{post.data?.title}</h2>
106-
<p>{post.data?.body}</p>
107-
<button onClick={onBack}>Back</button>
108-
</div>
146+
<>
147+
<h2>Edit post</h2>
148+
149+
<input ref={titleRef} defaultValue={post.data?.title} style={{ display: 'block' }} />
150+
151+
<textarea ref={bodyRef} defaultValue={post.data?.body} style={{ display: 'block', width: 400 }} />
152+
153+
<button
154+
onClick={() => {
155+
editPost.mutateAsync({
156+
params: { id: postId },
157+
body: { title: titleRef.current!.value, body: bodyRef.current!.value },
158+
});
159+
160+
setShowEdit(false);
161+
}}
162+
disabled={editPost.isPending}
163+
>
164+
Save
165+
</button>
166+
</>
109167
)}
110168

111169
<h3>Comments</h3>

examples/vite/src/client.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { QueryClient } from '@tanstack/react-query';
22
import { BuilderMutationCache } from 'react-query-builder';
33

44
export const queryClient = new QueryClient({
5+
defaultOptions: {
6+
queries: {
7+
staleTime: 1000 * 60,
8+
},
9+
},
510
mutationCache: new BuilderMutationCache(
611
{},
712
{

examples/vite/src/mocks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ window.addEventListener('storage', (event) => {
133133
});
134134

135135
const handlers = [
136-
http.get(`${baseUrl}/reset`, async () => {
136+
http.post(`${baseUrl}/reset`, async () => {
137137
await delay();
138138
const allData = recreateMockData();
139139
users = allData.users;

src/builder/HttpMutationBuilder.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ export class HttpMutationBuilder<
1111
constructor(config?: WithOptional<MutationBuilderConfig<T>, 'queryFn'>) {
1212
const mergeVars = config?.mergeVars || mergeHttpVars;
1313
const queryFn = config?.queryFn || createHttpQueryFn<T>(mergeVars);
14-
super({ mergeVars, queryFn, ...config });
14+
super({
15+
mergeVars,
16+
queryFn,
17+
...config,
18+
vars: {
19+
method: 'post',
20+
...config?.vars,
21+
},
22+
});
1523
}
1624

1725
withBody<TBody>(body?: TBody): HttpMutationBuilder<PrettifyWithVars<T, { body: TBody }>> {

src/builder/MutationBuilder.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import {
22
MutationFilters,
3+
MutationFunction,
34
MutationKey,
45
QueryClient,
56
UseMutationOptions,
67
useIsMutating,
78
useMutation,
9+
useQueryClient,
810
} from '@tanstack/react-query';
911
import { mergeTagOptions } from '../tags/mergeTagOptions';
1012
import { QueryInvalidatesMetadata } from '../tags/types';
@@ -42,22 +44,32 @@ export class MutationBuilderFrozen<T extends BuilderTypeTemplate> {
4244
return mergeVars(list, this.config.mergeVars);
4345
};
4446

47+
getMutationFn: (queryClient: QueryClient, meta?: any) => MutationFunction<T['data'], T['vars']> = (
48+
queryClient,
49+
meta,
50+
) => {
51+
return async (vars) => {
52+
return this.config.queryFn({
53+
queryKey: [this.mergeVars([this.config.vars, vars])],
54+
meta,
55+
client: this.config.queryClient || queryClient,
56+
signal: new AbortController().signal,
57+
});
58+
};
59+
};
60+
4561
getMutationKey: (vars: T['vars']) => MutationKey = (vars) => {
4662
const mergedVars = this.mergeVars([this.config.vars, vars]);
4763
return [this.mutationKeyPrefix, mergedVars];
4864
};
4965

5066
getMutationOptions: (
67+
queryClient: QueryClient,
5168
opts?: MutationBuilderConfig<T>['options'],
52-
) => UseMutationOptions<T['data'], T['error'], T['vars']> = (opts) => {
69+
) => UseMutationOptions<T['data'], T['error'], T['vars']> = (queryClient, opts) => {
5370
return mergeMutationOptions([
5471
{
55-
mutationFn: async (vars) => {
56-
return this.config.queryFn({
57-
queryKey: [this.mergeVars([this.config.vars, vars])],
58-
meta: opts?.meta as any,
59-
});
60-
},
72+
mutationFn: this.getMutationFn(queryClient, opts?.meta),
6173
meta: {
6274
invalidates: this.config.invalidates,
6375
updates: this.config.updates,
@@ -72,7 +84,8 @@ export class MutationBuilderFrozen<T extends BuilderTypeTemplate> {
7284
useMutation: (
7385
opts?: MutationBuilderConfig<T>['options'],
7486
) => ReturnType<typeof useMutation<T['data'], T['error'], T['vars']>> = (opts) => {
75-
return useMutation(this.getMutationOptions(opts), this.config.queryClient);
87+
const queryClient = useQueryClient();
88+
return useMutation(this.getMutationOptions(queryClient, opts), this.config.queryClient);
7689
};
7790

7891
useIsMutating: (vars: T['vars'], filters?: MutationFilters<T['data'], T['error'], T['vars']>) => number = (
@@ -131,7 +144,7 @@ export type MutationBuilderConfig<T extends BuilderTypeTemplate> = QueryInvalida
131144
T['data'],
132145
T['error']
133146
> & {
134-
queryFn: BuilderQueryFn<T['data'], T['vars']>;
147+
queryFn: BuilderQueryFn<T>;
135148

136149
vars?: Partial<T['vars']>;
137150
mergeVars?: BuilderMergeVarsFn<T['vars']>;

src/builder/QueryBuilder.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
InvalidateOptions,
55
QueryClient,
66
QueryFilters,
7+
QueryFunction,
78
RefetchOptions,
89
ResetOptions,
910
SetDataOptions,
@@ -43,6 +44,12 @@ export class QueryBuilderFrozen<T extends BuilderTypeTemplate> {
4344
return mergeVars(list, this.config.mergeVars);
4445
};
4546

47+
getQueryFn: () => QueryFunction<T['data'], [T['vars']]> = () => {
48+
return ({ client, meta, queryKey, signal, pageParam }) => {
49+
return this.config.queryFn({ client, meta, queryKey, signal, pageParam });
50+
};
51+
};
52+
4653
getQueryKey: (vars: T['vars']) => DataTag<[T['vars']], T['data'], T['error']> = (vars) => {
4754
return [this.mergeVars([this.config.vars, vars])] as DataTag<[T['vars']], T['data'], T['error']>;
4855
};
@@ -53,7 +60,7 @@ export class QueryBuilderFrozen<T extends BuilderTypeTemplate> {
5360
) => UseQueryOptions<T['data'], T['error'], T['data'], [T['vars']]> & { queryFn: FunctionType } = (vars, opts) => {
5461
return mergeQueryOptions([
5562
{
56-
queryFn: this.config.queryFn,
63+
queryFn: this.getQueryFn(),
5764
queryKey: this.getQueryKey(vars),
5865
meta: { tags: this.config.tags },
5966
},
@@ -214,7 +221,7 @@ export class QueryBuilder<T extends BuilderTypeTemplate = BuilderTypeTemplate> e
214221
type Updater<T> = T | undefined | ((oldData: T | undefined) => T | undefined);
215222

216223
export type QueryBuilderConfig<T extends BuilderTypeTemplate> = {
217-
queryFn: BuilderQueryFn<T['data'], T['vars']>;
224+
queryFn: BuilderQueryFn<T>;
218225
vars?: Partial<T['vars']>;
219226
mergeVars?: BuilderMergeVarsFn<T['vars']>;
220227

src/builder/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { QueryFunctionContext, UseQueryResult } from '@tanstack/react-query';
1+
import { QueryFunction, UseQueryResult } from '@tanstack/react-query';
22
import { RequestError } from '../http/errors';
33
import { HttpRequestOptions } from '../http/types';
44
import { Prettify } from '../types/utils';
@@ -40,4 +40,4 @@ export type BuilderQueriesResult<T extends BuilderTypeTemplate> = QueriesResultI
4040
queryMap: QueriesResultMapType<T>;
4141
};
4242

43-
export type BuilderQueryFn<TData, TVars> = (context: Partial<QueryFunctionContext<[TVars]>>) => TData | Promise<TData>;
43+
export type BuilderQueryFn<T extends BuilderTypeTemplate> = QueryFunction<T['data'], [T['vars']]>;

src/builder/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const mergeHttpVars: BuilderMergeVarsFn<HttpBuilderTypeTemplate['vars']>
8383

8484
export function createHttpQueryFn<T extends HttpBuilderTypeTemplate>(
8585
mergeVarsFn: BuilderMergeVarsFn<T['vars']>,
86-
): BuilderQueryFn<T['data'], T['vars']> {
86+
): BuilderQueryFn<T> {
8787
return async ({ queryKey, signal, meta, pageParam }: any) => {
8888
const [vars] = queryKey || [];
8989
const mergedVars = mergeVarsFn(vars, pageParam);

src/http/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type HttpRequestPathParams = {
1515
[param: string]: PathParam;
1616
};
1717

18-
export type HttpMethod = 'get' | 'post' | 'put' | 'delete';
18+
export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
1919

2020
// biome-ignore lint/suspicious/noEmptyInterface: <explanation>
2121
export interface HttpRequestMeta {}

src/tags/mergeTagOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function mergeTagOptions<TVars = void, TData = any, TErr = any, TTag exte
99
tagsList = tagsList.filter(Boolean) as QueryTagOption<TVars, TData, TErr, TTag>[];
1010

1111
if (tagsList.length === 0) return undefined;
12-
if (tagsList.every((tag) => Array.isArray(tag))) return tagsList.flat() as TTag[];
12+
if (tagsList.every((tag) => Array.isArray(tag) || typeof tag !== 'function')) return tagsList.flat() as TTag[];
1313

1414
const callback: QueryTagCallback<TVars, TData, TErr, TTag> = (...args) =>
1515
tagsList.flatMap((tag) => {

0 commit comments

Comments
 (0)