Skip to content

Commit cabc5b7

Browse files
committed
add withMethod and withMeta
1 parent 63220a0 commit cabc5b7

File tree

10 files changed

+63
-37
lines changed

10 files changed

+63
-37
lines changed

README.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,25 @@ It uses the builder pattern, the best pattern that works with complex Typescript
1616

1717
- REST client using fetch API
1818
- Automaticly created query keys and easy invalidation
19+
- Customizable with middlewares
1920
- Tag based invalidation
2021
- Declarative optimistic updates
21-
- Ability to strongly type everything, from parameters to tags
22+
- Ability to strongly type everything
2223

2324
## Advantages
2425

2526
- 💪 Strong-typed
2627
- 🧩 Consistently structured
2728
- 🚀 Features out-of-the-box
28-
- ⚙️ Customizable
29+
- ⚙️ Customizable and extendable
2930
- 🪶 Zero dependencies
3031
- 🚢 SSR and Router compatible
3132

3233
## TODO
3334

3435
- Strong typed customizable tags
3536
- Infinite queries
37+
- OOTB Query Hash function
3638

3739
## Examples
3840

@@ -48,21 +50,16 @@ const baseMutation = new HttpMutationBuilder().withBaseUrl(baseUrl);
4850

4951
type PostData = { id: number; title: string; body: string; userId: number };
5052

51-
const postsQuery = baseQuery
52-
.withConfig({ tags: "refreshable" })
53-
.withConfig({ tags: { type: "posts", id: "LIST" } })
54-
.withPath("/posts")
55-
.withData<PostData[]>();
53+
const postsQuery = baseQuery.withTags("refreshable", "posts").withPath("/posts").withData<PostData[]>();
5654

57-
const deletePostMutation = baseMutation
58-
.withConfig({ invalidates: { type: "posts", id: "LIST" } })
59-
.withVars({ method: "delete" })
60-
.withPath("/posts/:id");
55+
const deletePostMutation = baseMutation.withUpdates("posts").withMethod("delete").withPath("/posts/:id");
6156

6257
export function MyApp() {
6358
const posts = postsQuery.useQuery({});
6459
const deletePost = deletePostMutation.useMutation();
6560

61+
const [refresh] = useOperateOnTags({ tags: "refreshable" });
62+
6663
const onDelete = (id: number) => deletePost.mutateAsync({ params: { id } });
6764

6865
if (!posts.isSuccess) return <>Loading...</>;
@@ -79,6 +76,8 @@ export function MyApp() {
7976
</button>
8077
</div>
8178
))}
79+
80+
<button onClick={() => refresh()}>Refresh all posts</button>
8281
</>
8382
);
8483
}

examples/vite/src/App.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const commentsQuery = baseQuery
4040

4141
const editPostMutation = baseMutation
4242
.withPath('/posts/:id')
43-
.withVars({ method: 'put' })
43+
.withMethod('put')
4444
.withBody<Partial<PostData>>()
4545
.withUpdates<PostData | PostData[]>(
4646
{
@@ -68,7 +68,7 @@ const editPostMutation = baseMutation
6868
});
6969

7070
const deletePostMutation = baseMutation
71-
.withVars({ method: 'delete' })
71+
.withMethod('delete')
7272
.withPath('/posts/:id')
7373
.withUpdates<PostData[]>({
7474
type: 'posts' as any,
@@ -89,7 +89,7 @@ function App() {
8989
(x) => x.state.variables?.params.id,
9090
);
9191

92-
const [refresh] = useOperateOnTags({ tags: ['refreshable'] });
92+
const [refresh] = useOperateOnTags({ tags: 'refreshable' });
9393

9494
if (postId) return <PostPage postId={postId} onBack={() => setPostId(null)} />;
9595

src/builder/HttpMutationBuilder.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExtractPathParams } from '../http/types';
1+
import { ExtractPathParams, HttpMethod } from '../http/types';
22
import { WithOptional } from '../types/utils';
33
import { HttpQueryBuilder } from './HttpQueryBuilder';
44
import { MutationBuilder, MutationBuilderConfig } from './MutationBuilder';
@@ -59,6 +59,11 @@ export class HttpMutationBuilder<
5959
return this.withVars({ search }) as any;
6060
}
6161

62+
withMeta<TMeta>(meta?: TMeta): HttpMutationBuilder<AppendVarsType<T, { meta: TMeta }>> {
63+
if (!meta) return this as any;
64+
return this.withVars({ meta }) as any;
65+
}
66+
6267
withPath<const TPath extends string>(
6368
path: TPath,
6469
): ExtractPathParams<TPath> extends void
@@ -71,6 +76,10 @@ export class HttpMutationBuilder<
7176
return this.withVars({ baseUrl }) as any;
7277
}
7378

79+
withMethod(method: HttpMethod): this {
80+
return this.withVars({ method }) as any;
81+
}
82+
7483
declare withData: <TData>() => HttpMutationBuilder<SetDataType<T, TData>>;
7584
declare withError: <TError>() => HttpMutationBuilder<SetErrorType<T, TError>>;
7685
declare withVars: <TVars = T['vars'], const TReset extends boolean = false>(

src/builder/HttpQueryBuilder.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExtractPathParams } from '../http/types';
1+
import { ExtractPathParams, HttpMethod } from '../http/types';
22
import { WithOptional } from '../types/utils';
33
import { HttpMutationBuilder } from './HttpMutationBuilder';
44
import { MutationBuilder } from './MutationBuilder';
@@ -49,6 +49,11 @@ export class HttpQueryBuilder<T extends HttpBuilderTypeTemplate = HttpBuilderTyp
4949
return this.withVars({ search }) as any;
5050
}
5151

52+
withMeta<TMeta>(meta?: TMeta): HttpQueryBuilder<AppendVarsType<T, { meta: TMeta }>> {
53+
if (!meta) return this as any;
54+
return this.withVars({ meta }) as any;
55+
}
56+
5257
withPath<const TPath extends string>(
5358
path: TPath,
5459
): ExtractPathParams<TPath> extends void
@@ -61,6 +66,10 @@ export class HttpQueryBuilder<T extends HttpBuilderTypeTemplate = HttpBuilderTyp
6166
return this.withVars({ baseUrl }) as any;
6267
}
6368

69+
withMethod(method: HttpMethod): this {
70+
return this.withVars({ method }) as any;
71+
}
72+
6473
declare withData: <TData>() => HttpQueryBuilder<SetDataType<T, TData>>;
6574
declare withError: <TError>() => HttpQueryBuilder<SetErrorType<T, TError>>;
6675
declare withVars: <TVars = T['vars'], const TReset extends boolean = false>(

src/builder/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ export const mergeHttpVars: BuilderMergeVarsFn<HttpBuilderTypeTemplate['vars']>
8080
export function createHttpQueryFn<T extends HttpBuilderTypeTemplate>(
8181
mergeVarsFn: BuilderMergeVarsFn<T['vars']>,
8282
): BuilderQueryFn<T> {
83-
return async ({ queryKey, signal, meta, pageParam }: any) => {
83+
return async ({ queryKey, signal, pageParam }) => {
8484
const [vars] = queryKey || [];
85-
const mergedVars = mergeVarsFn(vars, pageParam);
86-
return httpRequest<T['data']>({ ...(mergedVars as any), meta, signal });
85+
const mergedVars = mergeVarsFn(vars, pageParam as Partial<T['vars']>);
86+
return httpRequest<T['data']>({ ...(mergedVars as any), signal });
8787
};
8888
}
8989

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' | 'patch' | 'delete';
18+
export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options';
1919

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

src/tags/operateOnTags.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
type QueryFilters,
77
hashKey,
88
} from '@tanstack/react-query';
9-
import type { QueryTag } from './types';
9+
import { resolveTags } from './resolveTags';
10+
import type { QueryTag, QueryTagStaticOption } from './types';
1011

1112
/**
1213
* Based on the behavior described in https://redux-toolkit.js.org/rtk-query/usage/automated-refetching#tag-invalidation-behavior
@@ -47,20 +48,22 @@ export function operateOnTags(
4748
queryClient,
4849
operation = 'invalidate',
4950
}: {
50-
tags: readonly QueryTag[];
51+
tags: QueryTagStaticOption;
5152
queryClient: QueryClient;
5253
operation?: OperateOnTagsOperation;
5354
},
5455
filters?: InvalidateQueryFilters,
5556
options?: InvalidateOptions,
5657
) {
57-
if (!tags?.length) return Promise.resolve();
58+
const resolvedTags = resolveTags({ tags, client: queryClient, vars: undefined });
59+
if (!resolvedTags?.length) return Promise.resolve();
5860

5961
const filtersObj: QueryFilters = {
6062
...(operation === 'refetch' && { type: 'active' }),
6163
...filters,
6264
predicate: (query) =>
63-
tags.some((tag) => queryMatchesTag(queryClient, query, tag)) && (!filters?.predicate || filters.predicate(query)),
65+
resolvedTags.some((tag) => queryMatchesTag(queryClient, query, tag)) &&
66+
(!filters?.predicate || filters.predicate(query)),
6467
};
6568

6669
if (operation === 'refetch') return queryClient.refetchQueries(filtersObj, options);

src/tags/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@ export type QueryTagCallback<TVars = void, TData = unknown, TErr = unknown, TTag
2222
ctx: QueryTagContext<TVars, TData, TErr>,
2323
) => TTag | readonly TTag[];
2424

25+
export type QueryTagStaticOption<TTag extends QueryTagObject = QueryTagObject> =
26+
| '*'
27+
| QueryTag<TTag>
28+
| readonly QueryTag<TTag>[];
29+
2530
export type QueryTagOption<
2631
TVars = void,
2732
TData = unknown,
2833
TErr = unknown,
2934
TTag extends QueryTagObject = QueryTagObject,
30-
> = '*' | QueryTag<TTag> | readonly QueryTag<TTag>[] | QueryTagCallback<TVars, TData, TErr, TTag>;
35+
> = QueryTagStaticOption<TTag> | QueryTagCallback<TVars, TData, TErr, TTag>;
3136

3237
export type QueryUpdaterFn<TVars = unknown, TData = unknown, TErr = unknown, TTarget = unknown> = (
3338
ctx: QueryTagContext<TVars, TData, TErr>,

src/tags/updaters.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function getUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTa
1414
if (updaterKey === 'body') return ctx.vars?.body;
1515
if (updaterKey === 'params') return ctx.vars?.params;
1616
if (updaterKey === 'search') return ctx.vars?.search;
17+
if (updaterKey === 'meta') return ctx.vars?.meta;
1718
return undefined;
1819
};
1920

@@ -34,8 +35,8 @@ export function getUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTa
3435
if (!target) return target;
3536

3637
const data = getFromCtx(ctx);
37-
const dataKey = data[byKey];
38-
if (!dataKey) return target;
38+
const dataKey = data?.[byKey];
39+
if (dataKey == null) return target;
3940

4041
if (Array.isArray(target)) {
4142
const foundIndex = target.findIndex((item) => item[byKey] === dataKey);
@@ -53,8 +54,8 @@ export function getUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTa
5354
if (!target) return target;
5455

5556
const data = getFromCtx(ctx);
56-
const dataKey = data[byKey];
57-
if (!dataKey) return target;
57+
const dataKey = data?.[byKey];
58+
if (dataKey == null) return target;
5859

5960
if (Array.isArray(target)) {
6061
const foundIndex = target.findIndex((item) => item[byKey] === dataKey);
@@ -71,8 +72,8 @@ export function getUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTa
7172
if (!target) return target;
7273

7374
const data = getFromCtx(ctx);
74-
const dataKey = data[byKey];
75-
if (!dataKey) return target;
75+
const dataKey = data?.[byKey];
76+
if (dataKey == null) return target;
7677

7778
if (Array.isArray(target)) {
7879
const foundIndex = target.findIndex((item) => item[byKey] === dataKey);
@@ -88,8 +89,8 @@ export function getUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTa
8889
if (!target || typeof target !== 'object') return target;
8990

9091
const data = getFromCtx(ctx);
91-
const dataKey = data[byKey];
92-
if (!dataKey) return target;
92+
const dataKey = data?.[byKey];
93+
if (dataKey == null) return target;
9394

9495
if (Array.isArray(target)) {
9596
return target.filter((item) => item[byKey] !== dataKey);
@@ -105,8 +106,8 @@ export function getUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTa
105106
if (!target) return target;
106107

107108
const data = getFromCtx(ctx);
108-
const dataKey = data[byKey];
109-
if (!dataKey) return target;
109+
const dataKey = data?.[byKey];
110+
if (dataKey == null) return target;
110111

111112
if (Array.isArray(target)) {
112113
const foundIndex = target.findIndex((item) => item[byKey] === dataKey);

src/tags/useOperateOnTags.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import {
66
useQueryClient,
77
} from '@tanstack/react-query';
88
import { type OperateOnTagsOperation, operateOnTags } from './operateOnTags';
9-
import type { QueryTag } from './types';
9+
import type { QueryTagStaticOption } from './types';
1010

1111
type OperateMutationOpts = {
12-
tags?: QueryTag[];
12+
tags?: QueryTagStaticOption;
1313
operation?: OperateOnTagsOperation;
1414
filters?: InvalidateQueryFilters;
1515
options?: InvalidateOptions;

0 commit comments

Comments
 (0)