-
-
Notifications
You must be signed in to change notification settings - Fork 289
Update BaseDataService to accommodate mutations, not just queries #9324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import { | |
| InfiniteData, | ||
| InvalidateOptions, | ||
| InvalidateQueryFilters, | ||
| MutationOptions, | ||
| OmitKeyof, | ||
| QueryClient, | ||
| QueryClientConfig, | ||
|
|
@@ -250,6 +251,37 @@ export class BaseDataService< | |
| return result.pages[pageIndex]; | ||
| } | ||
|
|
||
| /** | ||
| * Execute a mutation (a request that is expected to change the state of a server). | ||
| * Unlike `fetchQuery`, the request will not be cached. | ||
| * | ||
| * @param options - The options defining the mutation. Keep in mind that `mutationKey` and `mutationFn` are required when using data services. | ||
| * Additionally `retry` and `retryDelay` are not available, retries can be customized using the `servicePolicyOptions`. | ||
| * @returns The mutation results. | ||
| */ | ||
| protected async executeMutation< | ||
| TData extends Json, | ||
| TError = unknown, | ||
| TVariables = void, | ||
| TContext = unknown, | ||
| >( | ||
| options: WithRequired< | ||
| OmitKeyof< | ||
| MutationOptions<TData, TError, TVariables, TContext>, | ||
| 'retry' | 'retryDelay' | ||
| >, | ||
| 'mutationKey' | 'mutationFn' | ||
| >, | ||
| ): Promise<TData> { | ||
| const mutationCache = this.#queryClient.getMutationCache(); | ||
| const mutation = mutationCache.build(this.#queryClient, { | ||
| ...options, | ||
| mutationFn: (context) => | ||
| this.#policy.execute(() => options.mutationFn(context)), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, are we safe to apply the policy as-is to mutations? Just wondering if there could be a problem with accidentally doing double the mutation with retries 🤔
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, good point. We're still making an API request, so I feel like we would still want to have some kind of retry logic. But maybe it needs to be a little different than the logic used for GET requests. I wonder if the default retry policy for In Then I would be less worried here, because at that point, either we never make the request, or we do make the request but the server returns a 5xx. And in that case I feel like we ought to assume that the server is well-behaved, i.e. won't attempt to write to a database if it runs into an error. (If the server is not well-behaved, it shouldn't be our fault — the engineer writing the data service should know that and account for it.) What do you think about that idea?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improving the defaults to be more "targeted" to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't expect that most developers would configure the service policy, I would expect them to go with the defaults. But that's a good point. Should we have two kinds of service policies, one for queries and another for mutations? Then if a developer does configure a service policy, it should be more obvious what it's used for, and maybe it will cause them to think about it more critically. Or maybe this is solvable via documentation: we can add an "advanced" section to the tutorial/guide on data services that talks about the service policy, and we can remind the reader to consider non-GET requests when configuring it. |
||
| }); | ||
| return await mutation.execute(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mutation variables never forwardedMedium Severity
Reviewed by Cursor Bugbot for commit 603d673. Configure here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are using an older version of TanStack Query where |
||
| } | ||
|
|
||
| /** | ||
| * Invalidate queries serviced by this data service. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -44,6 +44,15 @@ export type GetActivityResponse = { | |||
| }; | ||||
| }; | ||||
|
|
||||
| export type AddFollowerResponse = { | ||||
| followed: { | ||||
| profileId: string; | ||||
| address: string; | ||||
| name: string; | ||||
| imageUrl?: string | null; | ||||
| }[]; | ||||
| }; | ||||
|
|
||||
| export type PageParam = | ||||
| | { | ||||
| before: string; | ||||
|
|
@@ -60,6 +69,8 @@ export class ExampleDataService extends BaseDataService< | |||
|
|
||||
| readonly #tokensBaseUrl = 'https://tokens.api.cx.metamask.io'; | ||||
|
|
||||
| readonly #socialBaseUrl = 'https://social.api.cx.metamask.io'; | ||||
|
|
||||
| constructor(messenger: ExampleMessenger) { | ||||
| super({ | ||||
| name: serviceName, | ||||
|
|
@@ -139,6 +150,29 @@ export class ExampleDataService extends BaseDataService< | |||
| ); | ||||
| } | ||||
|
|
||||
| async addFollower(followerId: string): Promise<AddFollowerResponse> { | ||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adapted from SocialService:
|
||||
| return this.executeMutation<AddFollowerResponse>({ | ||||
| mutationKey: [`${this.name}:addFollower`, followerId], | ||||
| mutationFn: async () => { | ||||
| const url = new URL(`${this.#socialBaseUrl}/api/v1/users/me/follows`); | ||||
|
|
||||
| const response = await fetch(url, { | ||||
| method: 'PUT', | ||||
| headers: { 'Content-Type': 'application/json' }, | ||||
| body: JSON.stringify({ followerId }), | ||||
| }); | ||||
|
|
||||
| if (!response.ok) { | ||||
| throw new Error( | ||||
| `Mutation failed with status code: ${response.status}.`, | ||||
| ); | ||||
| } | ||||
|
|
||||
| return response.json(); | ||||
| }, | ||||
| }); | ||||
| } | ||||
|
|
||||
| destroy(): void { | ||||
| super.destroy(); | ||||
| } | ||||
|
|
||||


Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QueryClientdoesn't have aexecuteMutationmethod itself. I looked inside of the class, however, and saw that there was agetMutationCachemethod and followed where that led. This is not howuseMutationworks, which usesMutationObserver, but I don't know if that really matters. I figured it made more sense to mimic howfetchQueryworks. But I'm not very familiar with TanStack Query, so if this is not the way we should be doing things, I'm happy to change this.