Skip to content

Commit 04135b6

Browse files
committed
add predefined updater functions
1 parent 3046873 commit 04135b6

File tree

6 files changed

+177
-19
lines changed

6 files changed

+177
-19
lines changed

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ It uses the builder pattern, the best pattern that works with complex Typescript
3232
## TODO
3333

3434
- Strong typed customizable tags
35-
- Middlewares on builder
36-
- Tags and updates as middlewares - withUpdates, withTags
37-
- Predefined update functions - clear, merge, replace, insert-by-id, delete-by-id, update-by-id, upsert-by-id
3835

3936
## Examples
4037

examples/vite/src/App.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,39 @@ const editPostMutation = baseMutation
4242
.withPath('/posts/:id')
4343
.withVars({ method: 'put' })
4444
.withBody<Partial<PostData>>()
45-
.withUpdates<PostData>({ type: 'posts' as any, id: 'LIST' }, (ctx) => ({
46-
type: 'posts' as any,
47-
id: ctx.vars.params.id,
48-
optimistic: true,
49-
updater(ctx, target) {
50-
return { ...target!, ...ctx.vars.body };
45+
.withUpdates<PostData | PostData[]>(
46+
{
47+
type: 'posts' as any,
48+
id: 'LIST',
49+
optimistic: true,
50+
updater: 'update-body-by-id',
5151
},
52-
}))
52+
(ctx) => ({
53+
type: 'posts' as any,
54+
id: ctx.vars.params.id,
55+
optimistic: true,
56+
updater: 'merge-body',
57+
}),
58+
)
5359
.withMiddleware(async (ctx, next) => {
5460
const res = await next({
5561
...ctx,
56-
vars: { ...ctx.vars, body: { ...ctx.vars.body, body: `${ctx.vars.body.body} \n Last updated ${new Date()}` } },
62+
vars: {
63+
...ctx.vars,
64+
body: { ...ctx.vars.body, body: `${ctx.vars.body.body} \n Last updated ${new Date().toISOString()}` },
65+
},
5766
});
5867
return res;
5968
});
6069

6170
const deletePostMutation = baseMutation
6271
.withVars({ method: 'delete' })
6372
.withPath('/posts/:id')
64-
.withUpdates({
73+
.withUpdates<PostData[]>({
6574
type: 'posts' as any,
6675
id: 'LIST',
6776
optimistic: true,
68-
updater(ctx, target) {
69-
return (target as PostData[]).filter((post) => post.id !== (ctx.vars as any).params.id);
70-
},
77+
updater: 'delete-params-by-id',
7178
});
7279

7380
function App() {
@@ -170,7 +177,11 @@ function PostPage({ postId, onBack }: { postId: number; onBack: () => void }) {
170177
onClick={() => {
171178
editPost.mutateAsync({
172179
params: { id: postId },
173-
body: { title: titleRef.current!.value, body: bodyRef.current!.value },
180+
body: {
181+
id: postId,
182+
title: titleRef.current!.value,
183+
body: bodyRef.current!.value,
184+
},
174185
});
175186

176187
setShowEdit(false);

src/tags/types.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { QueryClient } from '@tanstack/react-query';
2-
import { StringLiteral } from '../types/utils';
2+
import { KeysOfValue, StringLiteral } from '../types/utils';
33
import type { QueryTagType } from './tag-types';
44

55
/**
@@ -29,11 +29,31 @@ export type QueryTagOption<
2929
TTag extends QueryTagObject = QueryTagObject,
3030
> = '*' | QueryTag<TTag> | readonly QueryTag<TTag>[] | QueryTagCallback<TVars, TData, TErr, TTag>;
3131

32-
export type QueryUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTarget = unknown> = (
32+
export type QueryUpdaterFn<TVars = unknown, TData = unknown, TErr = unknown, TTarget = unknown> = (
3333
ctx: QueryTagContext<TVars, TData, TErr>,
3434
target: TTarget,
3535
) => TTarget;
3636

37+
export type QueryUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTarget = unknown> =
38+
| QueryUpdaterFn<TVars, TData, TErr, TTarget>
39+
| PredefinedUpdater<TVars, TData, TErr, TTarget>;
40+
41+
export type PredefinedUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTarget = unknown> =
42+
| `clear-${UpdaterSelector<TVars>}`
43+
| `merge-${UpdaterSelector<TVars>}`
44+
| `replace-${UpdaterSelector<TVars>}`
45+
| `${'create' | 'update' | 'upsert' | 'delete' | 'switch'}-${UpdaterSelector<TVars>}-by-${KeyOfTarget<TTarget>}`;
46+
47+
type UpdaterSelector<TVars> = 'data' | 'vars' | (KeysOfValue<TVars, Record<string, any>> & string);
48+
type KeyOfTarget<TTarget> = string & KeyOfItem<TTarget>;
49+
type KeyOfItem<TTarget> = TTarget extends readonly (infer TItem)[]
50+
? keyof TItem
51+
: TTarget extends Record<string, infer TItem>
52+
? TItem extends Record<string, any>
53+
? keyof TItem
54+
: never
55+
: never;
56+
3757
export type QueryUpdateTagObject<
3858
TVars = unknown,
3959
TData = unknown,

src/tags/updateTags.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useStableCallback } from '../hooks/useStableCallback';
33
import type { WithOptional } from '../types/utils';
44
import { queryMatchesTag } from './operateOnTags';
55
import type { QueryTagContext, QueryUpdateTag } from './types';
6+
import { getUpdater } from './updaters';
67

78
export type UpdateTagsUndoer = { hash: string; data: unknown };
89

@@ -34,6 +35,8 @@ export function updateTags({
3435

3536
const updater = typeof tag === 'object' && tag.updater;
3637
if (!updater) continue;
38+
const updaterFn = getUpdater(updater, tag);
39+
if (!updaterFn) continue;
3740

3841
/**
3942
* If the tag has an invalidate property, we will set the fetchStatus to 'fetching' to indicate that the query is being updated.
@@ -46,7 +49,7 @@ export function updateTags({
4649
setDataToExistingQuery(
4750
queryClient,
4851
q.queryHash,
49-
updater(ctx, q.state.data),
52+
updaterFn(ctx, q.state.data),
5053
willInvalidate ? { isInvalidated: true } : undefined,
5154
{ updated: optimistic ? 'optimistic' : 'pessimistic' },
5255
);

src/tags/updaters.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { QueryTagObject, QueryUpdater, QueryUpdaterFn } from './types';
2+
3+
export function getUpdater<TVars = unknown, TData = unknown, TErr = unknown, TTarget = unknown>(
4+
updater: QueryUpdater<TVars, TData, TErr, TTarget>,
5+
tag: QueryTagObject,
6+
): QueryUpdaterFn<TVars, TData, TErr, TTarget> | undefined {
7+
if (typeof updater === 'function') return updater;
8+
9+
const [_, updaterType, updaterKey, __, byKey] = updater.match(/^(\w+)-(\w+)(-by-(\w+))?$/) || [];
10+
11+
const getFromCtx = (ctx: any) => {
12+
if (updaterKey === 'data') return ctx.data;
13+
if (updaterKey === 'vars') return ctx.vars;
14+
if (updaterKey === 'body') return ctx.vars?.body;
15+
if (updaterKey === 'params') return ctx.vars?.params;
16+
if (updaterKey === 'search') return ctx.vars?.search;
17+
return undefined;
18+
};
19+
20+
if (updaterType === 'clear') {
21+
return (ctx, target) => undefined as unknown as TTarget;
22+
}
23+
24+
if (updaterType === 'merge') {
25+
return (ctx, target) => ({ ...target, ...getFromCtx(ctx) });
26+
}
27+
28+
if (updaterType === 'replace') {
29+
return (ctx, target) => getFromCtx(ctx) as TTarget;
30+
}
31+
32+
if (updaterType === 'create') {
33+
return (ctx, target): any => {
34+
if (!target) return target;
35+
36+
const data = getFromCtx(ctx);
37+
const dataKey = data[byKey];
38+
if (!dataKey) return target;
39+
40+
if (Array.isArray(target)) {
41+
const foundIndex = target.findIndex((item) => item[byKey] === dataKey);
42+
if (foundIndex < 0) return [...target, data];
43+
return target;
44+
}
45+
46+
if ((target as any)[byKey]) return target;
47+
return { ...target, [dataKey]: data };
48+
};
49+
}
50+
51+
if (updaterType === 'update') {
52+
return (ctx, target): any => {
53+
if (!target) return target;
54+
55+
const data = getFromCtx(ctx);
56+
const dataKey = data[byKey];
57+
if (!dataKey) return target;
58+
59+
if (Array.isArray(target)) {
60+
const foundIndex = target.findIndex((item) => item[byKey] === dataKey);
61+
if (foundIndex < 0) return target;
62+
return target.map((item, index) => (index === foundIndex ? data : item));
63+
}
64+
if ((target as any)[byKey]) return { ...target, [dataKey]: data };
65+
return target;
66+
};
67+
}
68+
69+
if (updaterType === 'upsert') {
70+
return (ctx, target): any => {
71+
if (!target) return target;
72+
73+
const data = getFromCtx(ctx);
74+
const dataKey = data[byKey];
75+
if (!dataKey) return target;
76+
77+
if (Array.isArray(target)) {
78+
const foundIndex = target.findIndex((item) => item[byKey] === dataKey);
79+
if (foundIndex < 0) return [...target, data];
80+
return target.map((item, index) => (index === foundIndex ? data : item));
81+
}
82+
return { ...target, [dataKey]: data };
83+
};
84+
}
85+
86+
if (updaterType === 'delete') {
87+
return (ctx, target): any => {
88+
if (!target || typeof target !== 'object') return target;
89+
90+
const data = getFromCtx(ctx);
91+
const dataKey = data[byKey];
92+
if (!dataKey) return target;
93+
94+
if (Array.isArray(target)) {
95+
return target.filter((item) => item[byKey] !== dataKey);
96+
}
97+
if (!(byKey in target)) return target;
98+
const { [String(dataKey)]: _, ...rest } = target as any;
99+
return rest;
100+
};
101+
}
102+
103+
if (updaterType === 'switch') {
104+
return (ctx, target): any => {
105+
if (!target) return target;
106+
107+
const data = getFromCtx(ctx);
108+
const dataKey = data[byKey];
109+
if (!dataKey) return target;
110+
111+
if (Array.isArray(target)) {
112+
const foundIndex = target.findIndex((item) => item[byKey] === dataKey);
113+
if (foundIndex < 0) return target;
114+
return target.map((item, index) => (index === foundIndex ? data : item));
115+
}
116+
if ((target as any)[byKey]) return { ...target, [dataKey]: data };
117+
return target;
118+
};
119+
}
120+
121+
return undefined;
122+
}

src/types/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ export type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>
1010
export type WithRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
1111

1212
export type FunctionType = (...args: any[]) => any;
13+
14+
/** Extracts the keys of a type that have a value of a specific type. */
15+
export type KeysOfValue<T, TCondition> = {
16+
[K in keyof T]: T[K] extends TCondition ? K : never;
17+
}[keyof T];

0 commit comments

Comments
 (0)