-
Notifications
You must be signed in to change notification settings - Fork 29
feat: migrate bounty mutation hooks to GraphQL #108
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 4 commits
4e8bdc2
92eb248
c1bd1e1
372d7e5
d9f66d5
faa007f
a0970f5
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 | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,98 +1,210 @@ | ||||||||||||||||||||||
| import { useMutation, useQueryClient } from '@tanstack/react-query'; | ||||||||||||||||||||||
| import { useMutation, useQueryClient } from "@tanstack/react-query"; | ||||||||||||||||||||||
| import { | ||||||||||||||||||||||
| bountiesApi, | ||||||||||||||||||||||
| type Bounty, | ||||||||||||||||||||||
| type CreateBountyInput, | ||||||||||||||||||||||
| type UpdateBountyInput, | ||||||||||||||||||||||
| type PaginatedResponse, | ||||||||||||||||||||||
| } from '@/lib/api'; | ||||||||||||||||||||||
| import { bountyKeys } from './use-bounties'; | ||||||||||||||||||||||
| type Bounty, | ||||||||||||||||||||||
| type CreateBountyInput, | ||||||||||||||||||||||
| type UpdateBountyInput, | ||||||||||||||||||||||
| type PaginatedResponse, | ||||||||||||||||||||||
| } from "@/lib/api"; | ||||||||||||||||||||||
| import { fetcher } from "@/lib/graphql/client"; | ||||||||||||||||||||||
| import { bountyKeys } from "@/lib/query/query-keys"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const CREATE_BOUNTY_MUTATION = ` | ||||||||||||||||||||||
| mutation CreateBounty($input: CreateBountyInput!) { | ||||||||||||||||||||||
| createBounty(input: $input) { | ||||||||||||||||||||||
| id | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| `; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const UPDATE_BOUNTY_MUTATION = ` | ||||||||||||||||||||||
| mutation UpdateBounty($input: UpdateBountyInput!) { | ||||||||||||||||||||||
| updateBounty(input: $input) { | ||||||||||||||||||||||
| id | ||||||||||||||||||||||
| status | ||||||||||||||||||||||
| updatedAt | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| `; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const DELETE_BOUNTY_MUTATION = ` | ||||||||||||||||||||||
| mutation DeleteBounty($id: ID!) { | ||||||||||||||||||||||
| deleteBounty(id: $id) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| `; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const CLAIM_BOUNTY_MUTATION_STATUS = "IN_PROGRESS" as const; | ||||||||||||||||||||||
| // UI cache uses the REST status union where "claimed" represents GraphQL "IN_PROGRESS". | ||||||||||||||||||||||
| const CLAIM_BOUNTY_OPTIMISTIC_STATUS: Bounty["status"] = "claimed"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| type CreateBountyMutationResponse = { | ||||||||||||||||||||||
| createBounty: Pick<Bounty, "id">; | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| type UpdateBountyMutationResponse = { | ||||||||||||||||||||||
| updateBounty: Pick<Bounty, "id" | "status" | "updatedAt">; | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| type DeleteBountyMutationResponse = { | ||||||||||||||||||||||
| deleteBounty: boolean; | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| type UpdateBountyMutationInput = Omit<UpdateBountyInput, "status"> & { | ||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||
| status?: Bounty["status"] | typeof CLAIM_BOUNTY_MUTATION_STATUS; | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async function createBountyMutation( | ||||||||||||||||||||||
| input: CreateBountyInput, | ||||||||||||||||||||||
| ): Promise<Pick<Bounty, "id">> { | ||||||||||||||||||||||
| const response = await fetcher< | ||||||||||||||||||||||
| CreateBountyMutationResponse, | ||||||||||||||||||||||
| { input: CreateBountyInput } | ||||||||||||||||||||||
| >(CREATE_BOUNTY_MUTATION, { input })(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return response.createBounty; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async function updateBountyMutation( | ||||||||||||||||||||||
| input: UpdateBountyMutationInput, | ||||||||||||||||||||||
| ): Promise<Pick<Bounty, "id" | "status" | "updatedAt">> { | ||||||||||||||||||||||
| const response = await fetcher< | ||||||||||||||||||||||
| UpdateBountyMutationResponse, | ||||||||||||||||||||||
| { input: UpdateBountyMutationInput } | ||||||||||||||||||||||
| >(UPDATE_BOUNTY_MUTATION, { input })(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return response.updateBounty; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async function deleteBountyMutation(id: string): Promise<void> { | ||||||||||||||||||||||
| const response = await fetcher<DeleteBountyMutationResponse, { id: string }>( | ||||||||||||||||||||||
| DELETE_BOUNTY_MUTATION, | ||||||||||||||||||||||
| { id }, | ||||||||||||||||||||||
| )(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!response.deleteBounty) { | ||||||||||||||||||||||
| throw new Error(`deleteBounty returned false for id: ${id}`); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function useCreateBounty() { | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return useMutation({ | ||||||||||||||||||||||
| mutationFn: (data: CreateBountyInput) => bountiesApi.create(data), | ||||||||||||||||||||||
| onSuccess: () => { | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return useMutation({ | ||||||||||||||||||||||
| mutationFn: createBountyMutation, | ||||||||||||||||||||||
| onSuccess: () => { | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function useUpdateBounty() { | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return useMutation({ | ||||||||||||||||||||||
| mutationFn: ({ id, data }: { id: string; data: UpdateBountyInput }) => | ||||||||||||||||||||||
| bountiesApi.update(id, data), | ||||||||||||||||||||||
| onMutate: async ({ id, data }) => { | ||||||||||||||||||||||
| await queryClient.cancelQueries({ queryKey: bountyKeys.detail(id) }); | ||||||||||||||||||||||
| const previous = queryClient.getQueryData<Bounty>(bountyKeys.detail(id)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (previous) { | ||||||||||||||||||||||
| queryClient.setQueryData<Bounty>(bountyKeys.detail(id), { | ||||||||||||||||||||||
| ...previous, | ||||||||||||||||||||||
| ...data, | ||||||||||||||||||||||
| updatedAt: new Date().toISOString(), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return { previous, id }; | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onError: (_err, _vars, context) => { | ||||||||||||||||||||||
| if (context?.previous) { | ||||||||||||||||||||||
| queryClient.setQueryData(bountyKeys.detail(context.id), context.previous); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onSettled: (_data, _err, { id }) => { | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.detail(id) }); | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return useMutation({ | ||||||||||||||||||||||
| mutationFn: ({ id, data }: { id: string; data: UpdateBountyInput }) => | ||||||||||||||||||||||
| updateBountyMutation({ id, ...data }), | ||||||||||||||||||||||
| onMutate: async ({ id, data }) => { | ||||||||||||||||||||||
| await queryClient.cancelQueries({ queryKey: bountyKeys.detail(id) }); | ||||||||||||||||||||||
| const previous = queryClient.getQueryData<Bounty>(bountyKeys.detail(id)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (previous) { | ||||||||||||||||||||||
| queryClient.setQueryData<Bounty>(bountyKeys.detail(id), { | ||||||||||||||||||||||
| ...previous, | ||||||||||||||||||||||
| ...data, | ||||||||||||||||||||||
| updatedAt: new Date().toISOString(), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return { previous, id }; | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onError: (_err, _vars, context) => { | ||||||||||||||||||||||
| if (context?.previous) { | ||||||||||||||||||||||
| queryClient.setQueryData( | ||||||||||||||||||||||
| bountyKeys.detail(context.id), | ||||||||||||||||||||||
| context.previous, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onSettled: (_data, _err, { id }) => { | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.detail(id) }); | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function useDeleteBounty() { | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return useMutation({ | ||||||||||||||||||||||
| mutationFn: (id: string) => bountiesApi.delete(id), | ||||||||||||||||||||||
| onMutate: async (id) => { | ||||||||||||||||||||||
| await queryClient.cancelQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const previousLists = queryClient.getQueriesData<PaginatedResponse<Bounty>>({ | ||||||||||||||||||||||
| queryKey: bountyKeys.lists(), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| queryClient.setQueriesData<PaginatedResponse<Bounty>>( | ||||||||||||||||||||||
| { queryKey: bountyKeys.lists() }, | ||||||||||||||||||||||
| (old) => old ? { | ||||||||||||||||||||||
| ...old, | ||||||||||||||||||||||
| data: old.data.filter((b) => b.id !== id), | ||||||||||||||||||||||
| pagination: { ...old.pagination, total: old.pagination.total - 1 }, | ||||||||||||||||||||||
| } : old | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return { previousLists }; | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onError: (_err, _id, context) => { | ||||||||||||||||||||||
| context?.previousLists.forEach(([key, data]) => { | ||||||||||||||||||||||
| queryClient.setQueryData(key, data); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onSettled: () => { | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return useMutation({ | ||||||||||||||||||||||
| mutationFn: deleteBountyMutation, | ||||||||||||||||||||||
| onMutate: async (id) => { | ||||||||||||||||||||||
| await queryClient.cancelQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const previousLists = queryClient.getQueriesData< | ||||||||||||||||||||||
| PaginatedResponse<Bounty> | ||||||||||||||||||||||
| >({ | ||||||||||||||||||||||
| queryKey: bountyKeys.lists(), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| queryClient.setQueriesData<PaginatedResponse<Bounty>>( | ||||||||||||||||||||||
| { queryKey: bountyKeys.lists() }, | ||||||||||||||||||||||
| (old) => | ||||||||||||||||||||||
| old | ||||||||||||||||||||||
| ? { | ||||||||||||||||||||||
| ...old, | ||||||||||||||||||||||
| data: old.data.filter((b) => b.id !== id), | ||||||||||||||||||||||
| pagination: { | ||||||||||||||||||||||
| ...old.pagination, | ||||||||||||||||||||||
| total: old.pagination.total - 1, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| : old, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return { previousLists }; | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onError: (_err, _id, context) => { | ||||||||||||||||||||||
| context?.previousLists.forEach(([key, data]) => { | ||||||||||||||||||||||
| queryClient.setQueryData(key, data); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onSettled: () => { | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
|
Comment on lines
+205
to
+210
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.
🛠️ Proposed fix — gate `removeQueries` on success- onSettled: (_data, _err, id) => {
- queryClient.removeQueries({ queryKey: bountyKeys.detail(id) });
- queryClient.invalidateQueries({ queryKey: bountyKeys.lists() });
- },
+ onSettled: (_data, _err, id) => {
+ if (!_err) {
+ queryClient.removeQueries({ queryKey: bountyKeys.detail(id) });
+ }
+ queryClient.invalidateQueries({ queryKey: bountyKeys.lists() });
+ },📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function useClaimBounty() { | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return useMutation({ | ||||||||||||||||||||||
| mutationFn: (id: string) => bountiesApi.claim(id), | ||||||||||||||||||||||
| onSuccess: (data, id) => { | ||||||||||||||||||||||
| queryClient.setQueryData(bountyKeys.detail(id), data); | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return useMutation({ | ||||||||||||||||||||||
| mutationFn: (id: string) => | ||||||||||||||||||||||
| updateBountyMutation({ id, status: CLAIM_BOUNTY_MUTATION_STATUS }), | ||||||||||||||||||||||
| onMutate: async (id) => { | ||||||||||||||||||||||
| await queryClient.cancelQueries({ queryKey: bountyKeys.detail(id) }); | ||||||||||||||||||||||
| const previous = queryClient.getQueryData<Bounty>(bountyKeys.detail(id)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (previous) { | ||||||||||||||||||||||
| queryClient.setQueryData<Bounty>(bountyKeys.detail(id), { | ||||||||||||||||||||||
| ...previous, | ||||||||||||||||||||||
| status: CLAIM_BOUNTY_OPTIMISTIC_STATUS, | ||||||||||||||||||||||
| updatedAt: new Date().toISOString(), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return { previous, id }; | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onError: (_err, _id, context) => { | ||||||||||||||||||||||
| if (context?.previous) { | ||||||||||||||||||||||
| queryClient.setQueryData( | ||||||||||||||||||||||
| bountyKeys.detail(context.id), | ||||||||||||||||||||||
| context.previous, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onSettled: (_data, _err, id) => { | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.detail(id) }); | ||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 17574
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 1266
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 752
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 233
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 1525
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 752
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 565
🏁 Script executed:
# Check lib/query/sync/handlers.ts to see which bountyKeys it's importing head -10 lib/query/sync/handlers.tsRepository: boundlessfi/bounties
Length of output: 478
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 714
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 453
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 1016
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 888
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 276
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 514
Remove the duplicate
bountyKeysdefinition fromhooks/use-bounties.ts; this file still exports its own version, while the canonical definition now lives in@/lib/query/query-keys.ts.Multiple hooks and the sync handler still import from the old location in
hooks/use-bounties.ts(e.g.,hooks/use-infinite-bounties.ts,hooks/use-bounty.ts,hooks/Use-bounty-detail.ts, and criticallylib/query/sync/handlers.ts). This creates two conflictingbountyKeysinstances with different structure: the old one lacks the.infinite()method. Update all imports to use@/lib/query/query-keysinstead, then delete the duplicate export fromhooks/use-bounties.ts.Additionally, in
useDeleteBounty(line 205–208), theremoveQueriescall on the detail cache executes unconditionally inonSettled, which fires on both success and error. On error, this clears the cache for a bounty that still exists on the server. Consider removing the detail cache only on success, or add a check for error state.🤖 Prompt for AI Agents