feat: update GraphQL fragments and queries for bounty management#114
Conversation
- Added `createdBy` field to the `BountyFields` fragment. - Refactored bounty queries to use GraphQL documents and fragments. - Updated query options for listing and fetching bounties to utilize new GraphQL structure. - Enhanced prefetching functions to align with updated query parameters. - Modified query key generation to support new GraphQL queries. - Updated sync handlers to work with the new bounty fragment structure. - Improved server authentication to retrieve user session data more effectively.
|
@Ekene001 is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughRemoves REST bounty API and many Next.js app/api/bounties routes, migrates hooks/components to use GraphQL-generated queries/mutations and GraphQL types, adds async cross-environment access token utilities and GraphQL client auth changes, and adds a client-side magic-link verification page. Changes
Sequence Diagram(s)(Skipped) Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
> [!CAUTION]
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)lib/query/sync/handlers.ts (1)
13-25:⚠️ Potential issue | 🔴 CriticalCache shape mismatch causes runtime errors when search query cache exists.
setQueriesDatawithbountyKeys.lists()(["Bounties"]) prefix-matches all queries keyed under["Bounties", ...]. However,use-bounty-search.ts(lines 36-48) stores{ data: BountyFieldsFragment[] }(nopaginationproperty), while sync handlers assumePaginatedResponseshape with bothdataandpaginationfields.When
handleBountyCreatedorhandleBountyDeletedruns and the search query cache exists, accessingoldData.pagination.totalthrowsTypeError: Cannot read property 'total' of undefined.Add defensive guards in all three handlers:
Fix for all three handlers
queryClient.setQueriesData<PaginatedResponse<BountyFieldsFragment>>( { queryKey: bountyKeys.lists() }, (oldData) => { - if (!oldData) return oldData; + if (!oldData || !('pagination' in oldData)) return oldData; return { ...oldData, data: [bounty, ...oldData.data], pagination: { ...oldData.pagination, total: oldData.pagination.total + 1, }, }; }, );Apply the same guard in
handleBountyUpdatedandhandleBountyDeleted.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/query/sync/handlers.ts` around lines 13 - 25, The sync handlers (handleBountyCreated, handleBountyUpdated, handleBountyDeleted) assume old cache is PaginatedResponse and crash when a search cache stores {data: BountyFieldsFragment[]}; update each setQueriesData callback (the calls using queryClient.setQueriesData with bountyKeys.lists()) to defensively check that oldData exists and has the expected pagination and data shape (e.g., typeof oldData.pagination === 'object' and Array.isArray(oldData.data')); if the guard fails, return oldData unchanged; only perform the paginated modifications (adjusting data array and pagination.total) when the shape checks pass.
♻️ Duplicate comments (1)
lib/query/sync/handlers.ts (1)
39-47:⚠️ Potential issue | 🟠 MajorSame shape mismatch risk in
handleBountyUpdated.Same issue as above —
oldData.data.map(...)will fail on cache entries that don't have a.dataarray in the expected shape.🛡️ Defensive guard
queryClient.setQueriesData<PaginatedResponse<BountyFieldsFragment>>( { queryKey: bountyKeys.lists() }, (oldData) => { - if (!oldData) return oldData; + if (!oldData || !Array.isArray(oldData.data)) return oldData; return { ...oldData, data: oldData.data.map((b) => (b.id === bounty.id ? bounty : b)), }; }, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/query/sync/handlers.ts` around lines 39 - 47, The cache update in handleBountyUpdated risks crashing when oldData isn't the expected PaginatedResponse shape because it assumes oldData.data is an array; change the updater passed to queryClient.setQueriesData for bountyKeys.lists() to defensively check that oldData exists and Array.isArray(oldData.data) before mapping—if the check fails, simply return oldData (or return a safe normalized shape) so you only call oldData.data.map(...) when data is an array.
🧹 Nitpick comments (9)
lib/auth-utils.ts (1)
31-38: FallbackgetSession()call may add latency on every request.When the cookie is httpOnly (not accessible via
document.cookie), each call togetAccessToken()triggers a network round-trip viaauthClient.getSession(). Since the axios interceptor calls this on every request, consider caching the token client-side (e.g., in a module-level variable with a short TTL) to avoid repeated network calls.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/auth-utils.ts` around lines 31 - 38, The fallback getSession() in getAccessToken causes a network call on every request when cookies are httpOnly; update getAccessToken to use a module-level cached token with a short TTL (e.g., tokenCache/tokenExpiresAt) so that if a valid token exists you return it immediately, and only call authClient.getSession() when the cache is empty or expired; on successful authClient.getSession() populate the cache and expiry, and ensure the axios interceptor continues to call getAccessToken() (so it benefits from the cache) and clears the cache on auth errors or logout.hooks/use-bounty-mutations.ts (1)
175-209:useClaimBountyuses string literal"IN_PROGRESS"instead of theBountyStatusenum.The rest of the codebase (e.g.,
project-bounties.tsx) usesBountyStatus.InProgressfrom the generated enums. Using a raw string here could drift out of sync if the enum value changes.Proposed fix
+ import { BountyStatus } from "@/lib/graphql/generated"; // ... - ) => mutation.mutate({ input: { id, status: "IN_PROGRESS" } }, options), + ) => mutation.mutate({ input: { id, status: BountyStatus.InProgress } }, options), // ... - mutation.mutateAsync({ input: { id, status: "IN_PROGRESS" } }, options), + mutation.mutateAsync({ input: { id, status: BountyStatus.InProgress } }, options),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounty-mutations.ts` around lines 175 - 209, In useClaimBounty replace the hard-coded string "IN_PROGRESS" with the generated enum value (BountyStatus.InProgress) so the mutation uses the enum constant; update imports to include BountyStatus (if missing) and ensure the mutate/mutateAsync calls pass { input: { id, status: BountyStatus.InProgress } } to useUpdateBountyMutation so types (UpdateBountyMutationVariables) remain correct and consistent with other usages like project-bounties.tsx.hooks/Use-bounty-detail.ts (1)
6-12:useBountyDetailis redundant — it duplicatesuseBountyexactly.
useBountyDetail(id)anduseBounty(id)(called without options) are functionally identical:
- Both call
useBountyQuery({ id }, { enabled: Boolean(id) /* or !!id */ })- Both return
{ ...rest, data: data?.bounty … }The
UseBountyOptions.enabledoverride inuseBountymakes it a strict superset ofuseBountyDetail. Consider removinguseBountyDetailand having its consumers calluseBountydirectly.Additionally, the same
null-cast issue applies here — replace theascast with?? undefined:♻️ Proposed fix
return { ...rest, - data: data?.bounty as BountyFieldsFragment | undefined, + data: data?.bounty ?? undefined, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/Use-bounty-detail.ts` around lines 6 - 12, Remove the redundant hook useBountyDetail: instead of keeping a duplicate wrapper around useBountyQuery, update consumers to call useBounty directly (useBounty(id) already calls useBountyQuery({ id }, { enabled: Boolean(id) }) and returns the same shape). Delete the useBountyDetail function and any imports/usages of it, and where data typing was forced with "data?.bounty as BountyFieldsFragment | undefined" replace that pattern with a safe coalescing expression (data?.bounty ?? undefined) when refactoring remaining call sites to maintain the same runtime behavior and correct typing.hooks/use-bounty.ts (1)
16-19: Prefer?? undefinedto coercenulland maintain type fidelity.
data?.bountyis typed asBountyFieldsFragment | null | undefinedby the generated code (GraphQL fields can returnnullfor not-found). Theascast stripsnullfrom the TypeScript type without converting it at runtime, so consumers receivenullwhile TypeScript believes they receiveBountyFieldsFragment | undefined.♻️ Proposed fix
return { ...rest, - data: data?.bounty as BountyFieldsFragment | undefined, + data: data?.bounty ?? undefined, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounty.ts` around lines 16 - 19, The returned data field currently uses an unsafe cast ("data?.bounty as BountyFieldsFragment | undefined") which strips null from the static type but leaves null at runtime; replace the cast by coercing null to undefined using the nullish coalescing operator (e.g., use data?.bounty ?? undefined) and drop the "as" assertion so the returned type aligns with runtime values (refer to data?.bounty and BountyFieldsFragment in hooks/use-bounty.ts).lib/query/bounty-queries.ts (1)
47-53: Casting toBountyFieldsFragmentdiscards thesubmissionsfield from the type.The
BountyDocumentGraphQL query fetchessubmissions { ...SubmissionFields }alongside...BountyFields, but the cast on Line 52 narrows the TypeScript type toBountyFieldsFragmentwhich doesn't includesubmissions. At runtime the data is there, but consumers (e.g.bounty-sidebar.tsx) are forced to manually extend the type with& { submissions?: ... }, which is fragile and can drift.Consider returning the full
BountyQuery["bounty"]type instead, or creating a dedicated fragment/type that includes submissions.♻️ Minimal fix: use the query's own return type
export function bountyDetailQueryOptions(id: string) { - return queryOptions({ + return queryOptions<BountyQuery["bounty"]>({ queryKey: bountyKeys.detail(id), queryFn: async () => { const response = await fetcher<BountyQuery, { id: string }>( BountyDocument, { id }, )(); - return response.bounty as BountyFieldsFragment; + return response.bounty; }, enabled: !!id, }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/query/bounty-queries.ts` around lines 47 - 53, The queryFn is casting response.bounty to BountyFieldsFragment which drops the fetched submissions field; update queryFn to return the query's actual type (use BountyQuery["bounty"] or a dedicated type that includes submissions) instead of BountyFieldsFragment so consumers receive submissions without manual extension — locate queryFn in bounty-queries.ts (uses BountyDocument, BountyQuery, and BountyFieldsFragment) and change the returned type to the fuller BountyQuery["bounty"] (or create/return a fragment type that includes submissions).components/bounty-detail/bounty-badges.tsx (1)
3-4: Fallback toCOMPLETEDfor unknown status may mislead users.If an unrecognized status string arrives (e.g. from a future backend enum value), the badge will silently render as "Completed" with its grey styling, which could confuse users into thinking a bounty is done when it isn't. Consider falling back to a more neutral config (e.g.
DRAFTor a dedicatedUNKNOWNentry) that won't be misinterpreted as a terminal state. Same reasoning applies toFIXED_PRICEon Line 16, though the impact there is lower.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-badges.tsx` around lines 3 - 4, The StatusBadge currently falls back to STATUS_CONFIG.COMPLETED for unknown statuses which can mislead users; update the fallback to a neutral entry (add STATUS_CONFIG.UNKNOWN or use STATUS_CONFIG.DRAFT) and use that instead of COMPLETED in StatusBadge (and similarly replace the FIXED_PRICE fallback with the neutral config in the related price-badge logic). Locate STATUS_CONFIG, the StatusBadge function, and the price/fixed-price handling, add an UNKNOWN (or DRAFT) config with neutral text/styling, and change the fallback expressions to reference that neutral entry so unrecognized backend enums render safely.components/bounty/bounty-sidebar.tsx (2)
34-41: Side effect in render body — moveconsole.warninto auseEffect.This
console.warnruns on every render when the env flag is set. Placing side effects directly in the component body (outside ofuseEffect) is a React anti-pattern and will produce excessive log noise.♻️ Suggested fix
+ useEffect(() => { + if (process.env.NEXT_PUBLIC_MOCK_MAINTAINER === "true") { + console.warn( + "DEV: Mock maintainer enabled in components/bounty/bounty-sidebar.tsx — do NOT enable in production", + ); + } + }, []); + - if ( - typeof window !== "undefined" && - process.env.NEXT_PUBLIC_MOCK_MAINTAINER === "true" - ) { - console.warn( - "DEV: Mock maintainer enabled in components/bounty/bounty-sidebar.tsx — do NOT enable in production", - ); - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty/bounty-sidebar.tsx` around lines 34 - 41, Move the console.warn side effect out of the component render and into a React effect: inside the BountySidebar component (or whichever component contains the current typeof window && process.env.NEXT_PUBLIC_MOCK_MAINTAINER check), replace the top-level console.warn with a useEffect that runs on mount (e.g., useEffect(() => { if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_MOCK_MAINTAINER === "true") console.warn(...); }, [])); keep the same warning message and window/env checks but ensure they execute only inside the useEffect to avoid running on every render.
92-117: Mock completion flow should be tracked or removed before release.
handleMarkCompletedusessetTimeoutto simulate an API call. This is fine for development but should be wired to a real GraphQL mutation (consistent with the rest of this PR's migration) before shipping. Consider adding a// TODO:comment referencing a tracking issue if this isn't being addressed in this PR.Would you like me to open an issue to track replacing this mock with a real GraphQL mutation?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty/bounty-sidebar.tsx` around lines 92 - 117, handleMarkCompleted currently uses a setTimeout to simulate the completion API call; replace this mock with the actual GraphQL mutation flow (call the appropriate mutation, await its result, handle errors, and only then call setLoading(false), setCompleted(true), setRatingTarget(...) and setShowRating(true)). Update the function handleMarkCompleted to invoke the project's real completion mutation and derive contributorId/contributorName from the mutation response (falling back to bounty.createdBy as now), handle errors via the existing error/reporting pattern, and remove the setTimeout; if you can't implement the mutation in this PR add a clear TODO with a tracking issue reference (e.g., "// TODO: replace mock setTimeout with completeBounty mutation — see ISSUE-1234") so it is tracked before release.lib/query/query-keys.ts (1)
15-16: Unsafe castparams as BountyQueryInputerases theOmit<..., "page">constraint.The
infinitefactory acceptsOmit<BountyQueryInput, "page">but immediately casts toBountyQueryInput, defeating the purpose of theOmit. IfgetKeyactually accepts the narrower type, the cast is unnecessary; if it doesn't, consider adjusting thegetKeycall or the parameter type.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/query/query-keys.ts` around lines 15 - 16, The current infinite factory unsafely casts params to BountyQueryInput when calling useBountiesQuery.getKey, which discards the Omit<..., "page"> constraint; fix by removing the cast and either (a) update useBountiesQuery.getKey to accept Omit<BountyQueryInput,"page">, or (b) build a full BountyQueryInput before calling getKey by merging params with a safe default page value (so you call useBountiesQuery.getKey({ query: { ...(params || {}), page: <default> } })), or (c) change the infinite signature to accept the exact type getKey expects—reference the infinite factory, BountyQueryInput and useBountiesQuery.getKey to locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(auth)/auth/magic-link/verify/page.tsx:
- Around line 27-55: The verifyToken function reads callbackURL from
searchParams and passes it directly to router.push, creating an open-redirect;
fix by validating callbackURL is a safe relative path before using it (e.g.,
ensure it starts with "/" and does not include a protocol/host or backslashes),
and fall back to a safe default ("/bounty") when validation fails; update the
logic in verifyToken where callbackURL is obtained and before
router.push(callbackURL) to perform this check and use the validatedCallback (or
default) for toast/redirect.
In `@hooks/use-bounties.ts`:
- Around line 17-33: The totalPages calc in useBounties can divide by zero if
data.bounties.limit is 0; update useBounties to guard the denominator
(data.bounties.limit) before computing totalPages — e.g., treat zero or falsy
limit as 1 or return 0 totalPages when limit is <= 0. Locate the totalPages
expression in useBounties and replace Math.ceil(data.bounties.total /
data.bounties.limit) with a safe computation that uses
Math.max(data.bounties.limit, 1) or a conditional that yields 0 when limit <= 0
so Infinity is never produced.
- Around line 8-15: Remove the duplicate bountyKeys definition from
hooks/use-bounties.ts and instead import and re-export the centralized
bountyKeys from lib/query/query-keys.ts so the complete factory (including the
infinite key) is used; update any local references in hooks/use-bounties.ts to
use the imported bountyKeys; then update downstream files
hooks/use-infinite-bounties.ts and hooks/use-bounty-mutations.ts to import
bountyKeys from lib/query/query-keys.ts (so calls like
bountyKeys.infinite(params) resolve correctly).
In `@hooks/Use-bounty-detail.ts`:
- Around line 1-13: Rename the hook file from Use-bounty-detail.ts to
use-bounty-detail.ts to match the project hook naming convention; update any
imports that reference the old name (e.g., in bounty-detail-client.tsx) to
import useBountyDetail from "use-bounty-detail" (preserve the exported function
name useBountyDetail and the existing export), and ensure the repository/CI is
updated so case-sensitive filesystems see the rename as a true rename (delete
the old-cased file and add the new one in the same commit if your VCS requires
it).
In `@hooks/use-infinite-bounties.ts`:
- Around line 54-57: Remove the duplicate export of flattenBountyPages from the
hooks file: delete the local function definition
(flattenBountyPages(PaginatedResponse<BountyFieldsFragment>[] | undefined):
BountyFieldsFragment[]) and instead import the canonical implementation from the
shared query module (the re-export in lib/query index). Update any local
references to use the imported flattenBountyPages and remove the duplicate
symbol to keep a single source of truth.
- Line 16: Replace the non-canonical import of bountyKeys in
use-infinite-bounties.ts (currently pulling from hooks/use-bounties.ts) with the
centralized bountyKeys from lib/query/query-keys.ts and update the queryKey
construction to use bountyKeys.infinite(params) instead of
[...bountyKeys.lists(), "infinite", params]; ensure the hook now builds the
exact key shape used by lib/query/bounty-queries.ts (bountyKeys.infinite()
yields ["Bounties", { query: params }]) so cache lookups and invalidations are
consistent.
In `@lib/auth-utils.ts`:
- Around line 23-28: The regex building for extracting the cookie uses the
literal cookie name variable name ("boundless_auth.session_token") but the dot
is not escaped, so change the RegExp construction in the cookie lookup to escape
the dot (e.g., replace "." with "\\.") or otherwise escape regex metacharacters
in name before using it in new RegExp; update the expression that sets match
(document.cookie.match(new RegExp(...))) so it matches the exact cookie name and
continue returning decodeURIComponent(match[2]) as before.
In `@lib/query/query-keys.ts`:
- Around line 10-19: bountyKeys currently duplicates keys and breaks the
hierarchical pattern used elsewhere; update bountyKeys so its lists() and
details() build off a single base all key using spread syntax (e.g., make
lists() return [...bountyKeys.all, "lists"] and details() return
[...bountyKeys.all, "details"]) and keep detail(id) as before, ensuring any
invalidation calls that expect the hierarchy still work; alternatively, if the
broad all key is truly unused, remove bountyKeys.all and lists() together—but
prefer the spread-based restructuring to match authKeys/complianceKeys patterns
and maintain consistent invalidation scopes.
In `@lib/server-auth.ts`:
- Around line 15-37: getCurrentUser currently sets User.id to the raw session
token from getSessionCookie, causing authorization and lookup failures; instead
validate the session token and fetch the real user record (id, name, email,
etc.) before returning. Update getCurrentUser to: read the session token via
getSessionCookie, call the auth/session backend or authClient.getSession (or
your session-store lookup) with the request headers to validate the token and
retrieve the user payload, return null if validation fails, and populate the
returned User with the real user id and profile fields (not the session token).
Ensure callers still get null when no session cookie exists and log/handle
validation errors appropriately.
---
Outside diff comments:
In `@lib/query/sync/handlers.ts`:
- Around line 13-25: The sync handlers (handleBountyCreated,
handleBountyUpdated, handleBountyDeleted) assume old cache is PaginatedResponse
and crash when a search cache stores {data: BountyFieldsFragment[]}; update each
setQueriesData callback (the calls using queryClient.setQueriesData with
bountyKeys.lists()) to defensively check that oldData exists and has the
expected pagination and data shape (e.g., typeof oldData.pagination === 'object'
and Array.isArray(oldData.data')); if the guard fails, return oldData unchanged;
only perform the paginated modifications (adjusting data array and
pagination.total) when the shape checks pass.
---
Duplicate comments:
In `@lib/query/sync/handlers.ts`:
- Around line 39-47: The cache update in handleBountyUpdated risks crashing when
oldData isn't the expected PaginatedResponse shape because it assumes
oldData.data is an array; change the updater passed to
queryClient.setQueriesData for bountyKeys.lists() to defensively check that
oldData exists and Array.isArray(oldData.data) before mapping—if the check
fails, simply return oldData (or return a safe normalized shape) so you only
call oldData.data.map(...) when data is an array.
---
Nitpick comments:
In `@components/bounty-detail/bounty-badges.tsx`:
- Around line 3-4: The StatusBadge currently falls back to
STATUS_CONFIG.COMPLETED for unknown statuses which can mislead users; update the
fallback to a neutral entry (add STATUS_CONFIG.UNKNOWN or use
STATUS_CONFIG.DRAFT) and use that instead of COMPLETED in StatusBadge (and
similarly replace the FIXED_PRICE fallback with the neutral config in the
related price-badge logic). Locate STATUS_CONFIG, the StatusBadge function, and
the price/fixed-price handling, add an UNKNOWN (or DRAFT) config with neutral
text/styling, and change the fallback expressions to reference that neutral
entry so unrecognized backend enums render safely.
In `@components/bounty/bounty-sidebar.tsx`:
- Around line 34-41: Move the console.warn side effect out of the component
render and into a React effect: inside the BountySidebar component (or whichever
component contains the current typeof window &&
process.env.NEXT_PUBLIC_MOCK_MAINTAINER check), replace the top-level
console.warn with a useEffect that runs on mount (e.g., useEffect(() => { if
(typeof window !== "undefined" && process.env.NEXT_PUBLIC_MOCK_MAINTAINER ===
"true") console.warn(...); }, [])); keep the same warning message and window/env
checks but ensure they execute only inside the useEffect to avoid running on
every render.
- Around line 92-117: handleMarkCompleted currently uses a setTimeout to
simulate the completion API call; replace this mock with the actual GraphQL
mutation flow (call the appropriate mutation, await its result, handle errors,
and only then call setLoading(false), setCompleted(true), setRatingTarget(...)
and setShowRating(true)). Update the function handleMarkCompleted to invoke the
project's real completion mutation and derive contributorId/contributorName from
the mutation response (falling back to bounty.createdBy as now), handle errors
via the existing error/reporting pattern, and remove the setTimeout; if you
can't implement the mutation in this PR add a clear TODO with a tracking issue
reference (e.g., "// TODO: replace mock setTimeout with completeBounty mutation
— see ISSUE-1234") so it is tracked before release.
In `@hooks/Use-bounty-detail.ts`:
- Around line 6-12: Remove the redundant hook useBountyDetail: instead of
keeping a duplicate wrapper around useBountyQuery, update consumers to call
useBounty directly (useBounty(id) already calls useBountyQuery({ id }, {
enabled: Boolean(id) }) and returns the same shape). Delete the useBountyDetail
function and any imports/usages of it, and where data typing was forced with
"data?.bounty as BountyFieldsFragment | undefined" replace that pattern with a
safe coalescing expression (data?.bounty ?? undefined) when refactoring
remaining call sites to maintain the same runtime behavior and correct typing.
In `@hooks/use-bounty-mutations.ts`:
- Around line 175-209: In useClaimBounty replace the hard-coded string
"IN_PROGRESS" with the generated enum value (BountyStatus.InProgress) so the
mutation uses the enum constant; update imports to include BountyStatus (if
missing) and ensure the mutate/mutateAsync calls pass { input: { id, status:
BountyStatus.InProgress } } to useUpdateBountyMutation so types
(UpdateBountyMutationVariables) remain correct and consistent with other usages
like project-bounties.tsx.
In `@hooks/use-bounty.ts`:
- Around line 16-19: The returned data field currently uses an unsafe cast
("data?.bounty as BountyFieldsFragment | undefined") which strips null from the
static type but leaves null at runtime; replace the cast by coercing null to
undefined using the nullish coalescing operator (e.g., use data?.bounty ??
undefined) and drop the "as" assertion so the returned type aligns with runtime
values (refer to data?.bounty and BountyFieldsFragment in hooks/use-bounty.ts).
In `@lib/auth-utils.ts`:
- Around line 31-38: The fallback getSession() in getAccessToken causes a
network call on every request when cookies are httpOnly; update getAccessToken
to use a module-level cached token with a short TTL (e.g.,
tokenCache/tokenExpiresAt) so that if a valid token exists you return it
immediately, and only call authClient.getSession() when the cache is empty or
expired; on successful authClient.getSession() populate the cache and expiry,
and ensure the axios interceptor continues to call getAccessToken() (so it
benefits from the cache) and clears the cache on auth errors or logout.
In `@lib/query/bounty-queries.ts`:
- Around line 47-53: The queryFn is casting response.bounty to
BountyFieldsFragment which drops the fetched submissions field; update queryFn
to return the query's actual type (use BountyQuery["bounty"] or a dedicated type
that includes submissions) instead of BountyFieldsFragment so consumers receive
submissions without manual extension — locate queryFn in bounty-queries.ts (uses
BountyDocument, BountyQuery, and BountyFieldsFragment) and change the returned
type to the fuller BountyQuery["bounty"] (or create/return a fragment type that
includes submissions).
In `@lib/query/query-keys.ts`:
- Around line 15-16: The current infinite factory unsafely casts params to
BountyQueryInput when calling useBountiesQuery.getKey, which discards the
Omit<..., "page"> constraint; fix by removing the cast and either (a) update
useBountiesQuery.getKey to accept Omit<BountyQueryInput,"page">, or (b) build a
full BountyQueryInput before calling getKey by merging params with a safe
default page value (so you call useBountiesQuery.getKey({ query: { ...(params ||
{}), page: <default> } })), or (c) change the infinite signature to accept the
exact type getKey expects—reference the infinite factory, BountyQueryInput and
useBountiesQuery.getKey to locate the code.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (37)
app/(auth)/auth/magic-link/verify/page.tsxapp/api/bounties/[id]/applications/route.tsapp/api/bounties/[id]/apply/route.tsapp/api/bounties/[id]/claim/route.tsapp/api/bounties/[id]/competition/join/route.tsapp/api/bounties/[id]/join/route.tsapp/api/bounties/[id]/milestones/advance/route.tsapp/api/bounties/[id]/route.tsapp/api/bounties/[id]/submissions/route.tsapp/api/bounties/[id]/submit/route.tsapp/api/bounties/route.tscomponents/bounty-detail/bounty-badges.tsxcomponents/bounty-detail/bounty-detail-header-card.tsxcomponents/bounty-detail/bounty-detail-sidebar-cta.tsxcomponents/bounty/bounty-card.tsxcomponents/bounty/bounty-list.tsxcomponents/bounty/bounty-sidebar.tsxcomponents/projects/project-bounties.tsxhooks/Use-bounty-detail.tshooks/use-bounties.tshooks/use-bounty-mutations.tshooks/use-bounty-search.tshooks/use-bounty.tshooks/use-infinite-bounties.tslib/api/bounties.tslib/api/client.tslib/api/index.tslib/auth-utils.tslib/bounty-config.tslib/graphql/client.tslib/graphql/generated.tslib/graphql/operations/fragments.graphqllib/query/bounty-queries.tslib/query/prefetch.tslib/query/query-keys.tslib/query/sync/handlers.tslib/server-auth.ts
💤 Files with no reviewable changes (11)
- app/api/bounties/[id]/apply/route.ts
- app/api/bounties/[id]/claim/route.ts
- app/api/bounties/[id]/milestones/advance/route.ts
- app/api/bounties/[id]/submit/route.ts
- lib/api/bounties.ts
- app/api/bounties/[id]/join/route.ts
- app/api/bounties/[id]/competition/join/route.ts
- app/api/bounties/[id]/submissions/route.ts
- app/api/bounties/[id]/applications/route.ts
- app/api/bounties/route.ts
- app/api/bounties/[id]/route.ts
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
lib/server-auth.ts (2)
57-61: Loose type assertion on the response payload.The normalization at lines 58-61 uses
ascasts on the raw JSON without runtime validation. If the backend returns an unexpected shape (e.g.,{ data: null }or{ data: "string" }),normalizedcould benull/malformed and slip through. Thenormalized?.user?.idcheck on line 63 does provide a safety net, but consider adding a basic structural check (e.g., verifyingnormalizedis a non-null object) to avoid subtle issues.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/server-auth.ts` around lines 57 - 61, The JSON normalization uses unchecked casts (payload as ...) which can produce null/malformed normalized values; replace the cast with a runtime type guard that validates payload/data is a non-null object and conforms at least to the minimal SessionPayload shape (e.g., has a non-null object "user" with a string "id") before assigning to normalized (or return undefined otherwise). Implement a small type-guard helper like isSessionPayload(value): value is SessionPayload and use it when handling response.json() so normalized is only set when the structural check passes (then downstream checks like normalized?.user?.id are safe).
25-72: Session validation now fetches real user data — resolves the previously reported critical issue.The rewrite correctly validates the session against the backend and extracts the actual user identity, fixing the prior bug where the raw session token was used as
User.id.Two resilience concerns with
fetchValidatedSession:
No fetch timeout. If the auth endpoint is unresponsive, this blocks server component rendering indefinitely. Consider adding an
AbortSignal.timeout().Multi-endpoint fallback is asymmetric on 401/403. Lines 51-53 return
nullimmediately on the first endpoint's 401/403 without trying the second endpoint. If only the first endpoint is deployed (or vice versa), this is fine — but if the endpoints have different auth behaviors, the early return could mask a valid session on the second endpoint.🛡️ Suggested timeout addition
const url = new URL(endpoint, baseURL); const response = await fetch(url, { method: "GET", headers: { cookie: cookieHeader, authorization: `Bearer ${sessionToken}`, accept: "application/json", }, cache: "no-store", + signal: AbortSignal.timeout(5000), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/server-auth.ts` around lines 25 - 72, fetchValidatedSession currently can hang because fetch has no timeout and it returns early on a 401/403 from the first endpoint, which prevents trying the fallback endpoint; update fetchValidatedSession to use an AbortController/AbortSignal.timeout (or AbortController with setTimeout) when calling fetch so each request times out (ensure the controller is aborted and cleaned up), and change the 401/403 handling inside the endpoints loop (where response.status is checked) to continue to the next endpoint instead of immediately returning null so both endpoints are tried before returning null; reference the function fetchValidatedSession and the endpoints array when making these edits.lib/graphql/client.ts (1)
62-74: Multiple toast notifications fire for batch auth errors.If
gqlError.response.errorscontains more than one entry with an auth status, theforEachloop will triggertoast.error(...)and dispatchauth:unauthorizedfor each one. This results in duplicate toasts stacked for the user.♻️ Suggested fix: break after the first auth error
- if (gqlError?.response?.errors) { - gqlError.response.errors.forEach((err) => { - const status = err?.extensions?.status ?? 500; - if (isAuthStatus(status)) { - // Let the application handle unauthorized state, potentially redirecting to login - if (typeof window !== "undefined") { - toast.error("Your session has expired. Please log in again."); - window.dispatchEvent( - new CustomEvent("auth:unauthorized", { detail: { status } }), - ); - } - } - }); - } + if (gqlError?.response?.errors) { + const authErr = gqlError.response.errors.find((err) => + isAuthStatus(err?.extensions?.status ?? 500), + ); + if (authErr && typeof window !== "undefined") { + toast.error("Your session has expired. Please log in again."); + window.dispatchEvent( + new CustomEvent("auth:unauthorized", { + detail: { status: authErr.extensions?.status }, + }), + ); + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/graphql/client.ts` around lines 62 - 74, The loop over gqlError.response.errors can emit multiple auth handling actions; in the error-processing block (where gqlError, gqlError.response.errors, isAuthStatus, toast.error and the "auth:unauthorized" CustomEvent are used) change the logic to stop after the first authentication error is handled (e.g., break/return out of the loop or use .some/find to short-circuit) so that toast.error and window.dispatchEvent are invoked only once per GraphQL response.hooks/use-bounties.ts (1)
18-19:pageis derived from input params, not from the server response.Line 19 uses
params?.page ?? 1for the pagination page value. If the server normalizes or clamps the page (e.g., caps attotalPages), the client-side value won't reflect the server's actual page. Consider using apagefield from the GraphQL response if one is available.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounties.ts` around lines 18 - 19, The pagination.page in hooks/use-bounties.ts is set from the input params (params?.page ?? 1) which may diverge from the server-normalized page; update the hook to use the server-returned page field instead (for example, replace params?.page with the GraphQL response page such as data?.bounties?.page or data?.page depending on your schema) inside the pagination object so pagination reflects the server's clamp/normalization; adjust any typing and null checks accordingly in useBounties to fall back to 1 only if the server page is absent.hooks/use-infinite-bounties.ts (1)
15-48: SpecifyTPageParamgeneric to eliminatepageParam as numbercasts.In TanStack Query v5,
useInfiniteQueryaccepts aTPageParamgeneric parameter. Without it,pageParamdefaults tounknown, requiring theas numbercasts on lines 24 and 32. Passing the generic makes this type-safe.♻️ Suggested fix
- return useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>({ + return useInfiniteQuery< + PaginatedResponse<BountyFieldsFragment>, + Error, + InfiniteData<PaginatedResponse<BountyFieldsFragment>>, + ReturnType<typeof bountyKeys.infinite>, + number + >({ queryKey: bountyKeys.infinite(params), queryFn: async ({ pageParam }) => { const response = await fetcher< BountiesQuery, { query: BountyQueryInput } >(BountiesDocument, { query: { ...params, - page: pageParam as number, + page: pageParam, limit: params?.limit ?? DEFAULT_LIMIT, }, })(); const data = response.bounties; return { data: data.bounties as BountyFieldsFragment[], pagination: { - page: pageParam as number, + page: pageParam, limit: data.limit, total: data.total, totalPages: Math.ceil(data.total / data.limit), }, }; },Note: You'll need to add
import { type InfiniteData } from "@tanstack/react-query".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-infinite-bounties.ts` around lines 15 - 48, The issue is that useInfiniteQuery's pageParam is typed as unknown, causing unsafe "as number" casts in queryFn and pagination helpers; fix it by adding the TPageParam generic (number) to the useInfiniteQuery call so pageParam is typed as number (adjust the other generic type args as needed for PaginatedResponse<BountyFieldsFragment>), remove the "as number" casts in queryFn and pagination logic (queryFn, getNextPageParam, getPreviousPageParam, initialPageParam), and add the import for type InfiniteData from "@tanstack/react-query" if your surrounding code requires that type.hooks/use-bounty-mutations.ts (1)
198-198: Replace hardcoded status strings with theBountyStatusenum for type safety.The strings
"IN_PROGRESS"at lines 198-199 match the enum valueBountyStatus.InProgress = "IN_PROGRESS", but using the generated enum directly would provide better type safety and maintainability. ImportBountyStatusfrom@/lib/graphql/generatedand replace both occurrences withBountyStatus.InProgress.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounty-mutations.ts` at line 198, Replace the hardcoded status string "IN_PROGRESS" used in the mutation call with the generated enum value BountyStatus.InProgress for type safety: import { BountyStatus } from "@/lib/graphql/generated" and update the mutation invocation (the call site using mutation.mutate({ input: { id, status: "IN_PROGRESS" } }, options)) to use status: BountyStatus.InProgress; make the same replacement for any other occurrence in this file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@hooks/use-bounty-mutations.ts`:
- Around line 53-68: The optimistic update is reading and writing the wrong type
for the cache key used by bountyKeys.detail(id): change getQueryData and
setQueryData to use BountyQuery (not BountyFieldsFragment), import BountyQuery
from "@/lib/graphql/generated", read previous as const previous =
queryClient.getQueryData<BountyQuery>(bountyKeys.detail(id)), and when updating
call queryClient.setQueryData<BountyQuery>(bountyKeys.detail(id), { ...previous,
bounty: { ...previous!.bounty, ...variables.input, updatedAt: new
Date().toISOString() } }), preserving the { bounty: BountyFieldsFragment }
wrapper so useBountyDetail's data?.bounty remains valid.
- Around line 175-186: useClaimBounty writes data.updateBounty (a
BountyFieldsFragment) directly into the detail cache key which expects a
BountyQuery shape ({ bounty: ... }), causing useBountyDetail to see undefined;
update the onSuccess in useClaimBounty (the useUpdateBountyMutation callback) to
setQueryData for bountyKeys.detail(variables.input.id) with an object shaped
like { bounty: data.updateBounty } instead of data.updateBounty, preserving the
expected BountyQuery format and keep the existing queryClient.invalidateQueries
call.
In `@hooks/use-infinite-bounties.ts`:
- Line 35: The totalPages calculation in the use-infinite-bounties hook can
produce Infinity when data.limit is 0; update the logic that computes totalPages
(currently Math.ceil(data.total / data.limit)) to guard against zero or falsy
limits by treating a zero limit as 1 or returning 0 pages (e.g., if data.limit
<= 0 then totalPages = 0 else Math.ceil(data.total / data.limit)); apply this
change where totalPages is computed to ensure safe behavior when data.limit is
0.
---
Nitpick comments:
In `@hooks/use-bounties.ts`:
- Around line 18-19: The pagination.page in hooks/use-bounties.ts is set from
the input params (params?.page ?? 1) which may diverge from the
server-normalized page; update the hook to use the server-returned page field
instead (for example, replace params?.page with the GraphQL response page such
as data?.bounties?.page or data?.page depending on your schema) inside the
pagination object so pagination reflects the server's clamp/normalization;
adjust any typing and null checks accordingly in useBounties to fall back to 1
only if the server page is absent.
In `@hooks/use-bounty-mutations.ts`:
- Line 198: Replace the hardcoded status string "IN_PROGRESS" used in the
mutation call with the generated enum value BountyStatus.InProgress for type
safety: import { BountyStatus } from "@/lib/graphql/generated" and update the
mutation invocation (the call site using mutation.mutate({ input: { id, status:
"IN_PROGRESS" } }, options)) to use status: BountyStatus.InProgress; make the
same replacement for any other occurrence in this file.
In `@hooks/use-infinite-bounties.ts`:
- Around line 15-48: The issue is that useInfiniteQuery's pageParam is typed as
unknown, causing unsafe "as number" casts in queryFn and pagination helpers; fix
it by adding the TPageParam generic (number) to the useInfiniteQuery call so
pageParam is typed as number (adjust the other generic type args as needed for
PaginatedResponse<BountyFieldsFragment>), remove the "as number" casts in
queryFn and pagination logic (queryFn, getNextPageParam, getPreviousPageParam,
initialPageParam), and add the import for type InfiniteData from
"@tanstack/react-query" if your surrounding code requires that type.
In `@lib/graphql/client.ts`:
- Around line 62-74: The loop over gqlError.response.errors can emit multiple
auth handling actions; in the error-processing block (where gqlError,
gqlError.response.errors, isAuthStatus, toast.error and the "auth:unauthorized"
CustomEvent are used) change the logic to stop after the first authentication
error is handled (e.g., break/return out of the loop or use .some/find to
short-circuit) so that toast.error and window.dispatchEvent are invoked only
once per GraphQL response.
In `@lib/server-auth.ts`:
- Around line 57-61: The JSON normalization uses unchecked casts (payload as
...) which can produce null/malformed normalized values; replace the cast with a
runtime type guard that validates payload/data is a non-null object and conforms
at least to the minimal SessionPayload shape (e.g., has a non-null object "user"
with a string "id") before assigning to normalized (or return undefined
otherwise). Implement a small type-guard helper like isSessionPayload(value):
value is SessionPayload and use it when handling response.json() so normalized
is only set when the structural check passes (then downstream checks like
normalized?.user?.id are safe).
- Around line 25-72: fetchValidatedSession currently can hang because fetch has
no timeout and it returns early on a 401/403 from the first endpoint, which
prevents trying the fallback endpoint; update fetchValidatedSession to use an
AbortController/AbortSignal.timeout (or AbortController with setTimeout) when
calling fetch so each request times out (ensure the controller is aborted and
cleaned up), and change the 401/403 handling inside the endpoints loop (where
response.status is checked) to continue to the next endpoint instead of
immediately returning null so both endpoints are tried before returning null;
reference the function fetchValidatedSession and the endpoints array when making
these edits.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
app/(auth)/auth/magic-link/verify/page.tsxcomponents/bounty-detail/bounty-detail-client.tsxhooks/use-bounties.tshooks/use-bounty-detail.tshooks/use-bounty-mutations.tshooks/use-infinite-bounties.tslib/auth-utils.tslib/graphql/client.tslib/server-auth.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- app/(auth)/auth/magic-link/verify/page.tsx
- lib/auth-utils.ts
There was a problem hiding this comment.
♻️ Duplicate comments (2)
hooks/use-bounty-mutations.ts (2)
56-77: Previous cache-type issues are fully resolved — LGTM.Both
getQueryData<BountyQuery>(Line 56) andsetQueryData<BountyQuery>with the preserved{ ...previous, bounty: { ... } }wrapper (Lines 67–74) are now correct.useBountyDetail'sdata?.bountyaccess will remain valid after the optimistic update.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounty-mutations.ts` around lines 56 - 77, The optimistic cache update using queryClient.getQueryData<BountyQuery> and queryClient.setQueryData<BountyQuery> with the preserved wrapper around previous and bounty (using bountyKeys.detail(id)) is correct and requires no code changes; keep the current logic in the mutation (including the optimisticInput construction and updatedAt override) so useBountyDetail will continue to see data?.bounty as expected.
188-195: Previous cache-shape mismatch is resolved — LGTM.
queryClient.setQueryData<BountyQuery>now correctly wraps the result as{ bounty: data.updateBounty }(Line 191), matching the shapeuseBountyDetailexpects.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounty-mutations.ts` around lines 188 - 195, The cache shape mismatch has been resolved by wrapping the updated bounty result in the expected object shape; keep the onSuccess handler in the mutation (the queryClient.setQueryData<BountyQuery> call that sets bountyKeys.detail(variables.input.id) to { bounty: data.updateBounty } and the subsequent queryClient.invalidateQueries({ queryKey: bountyKeys.lists() })) as-is—no change needed.
🧹 Nitpick comments (4)
hooks/use-infinite-bounties.ts (1)
15-15: Dropping the explicit generic lets TypeScript inferTPageParam = number, removing theas numbercasts.With
useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>, onlyTQueryFnDatais fixed;TPageParamdefaults tounknown, which is whypageParam as numberis needed twice (lines 24 and 32). Removing the explicit generic allows TanStack Query v5 to inferTPageParam = numberdirectly frominitialPageParam: 1.♻️ Proposed refactor
- return useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>({ + return useInfiniteQuery({ queryKey: bountyKeys.infinite(params), queryFn: async ({ pageParam }) => { const response = await fetcher< BountiesQuery, { query: BountyQueryInput } >(BountiesDocument, { query: { ...params, - page: pageParam as number, + page: pageParam, limit: params?.limit ?? DEFAULT_LIMIT, }, })(); const data = response.bounties; return { data: data.bounties as BountyFieldsFragment[], pagination: { - page: pageParam as number, + page: pageParam, limit: data.limit,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-infinite-bounties.ts` at line 15, Remove the explicit generic on useInfiniteQuery so TanStack Query can infer TPageParam as number (i.e., change the call to useInfiniteQuery(...) instead of useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>), keep initialPageParam: 1, and then remove the redundant pageParam as number casts found in the fetch function and getNextPageParam logic (references: useInfiniteQuery call, initialPageParam, and the occurrences of pageParam as number). This will let the hook infer page param type correctly and eliminate the unnecessary type assertions.hooks/use-bounty-mutations.ts (3)
61-65: Null filter in optimistic update silently drops intentional field-clearing.Filtering
nullvalues alongsideundefinedmeans that if a consumer explicitly sets a field tonullto clear it (valid for nullable GraphQL fields), the optimistic update will not reflect that change — it will keep the old value until the server confirms. Onlyundefined(i.e. "not provided") should be excluded.♻️ Proposed fix
- ([, value]) => value !== undefined && value !== null, + ([, value]) => value !== undefined,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounty-mutations.ts` around lines 61 - 65, The optimistic update currently removes both undefined and null values when building optimisticInput in the optimistic update (see the optimisticInput creation that filters Object.entries(variables.input)), which prevents intentionally-cleared nullable fields from being represented; change the filter to only exclude undefined (i.e., ([, value]) => value !== undefined) so null values are preserved in optimisticInput and null-clearing is reflected immediately in the UI.
97-119:MutateOptionsexposes the internal variable shape, not the wrapper's public input shape.The wrapper changes the public API from
{ input: UpdateBountyInput }to{ id, data }, but theoptionsparameter is still typed asMutateOptions<UpdateBountyMutation, unknown, UpdateBountyMutationVariables, unknown>. Sinceoptionsis passed through to the underlying mutation, callbacks likeonSuccess(data, variables)will receivevariables = { input: UpdateBountyInput }at runtime — not{ id, data }as a consumer might expect from the wrapper's call signature. The same applies touseDeleteBounty(whose consumers callmutate(id: string)but callbacks receive{ id: string }).If per-call callback consumers are uncommon, this is low priority. To fix it properly, define wrapper-local
MutateOptionstypes that match the public input and intercept/adapt the callbacks before forwarding to the underlying mutation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounty-mutations.ts` around lines 97 - 119, The current mutate/mutateAsync signatures (in hooks/use-bounty-mutations.ts) accept public inputs ({ id, data }) but still type options as MutateOptions<..., UpdateBountyMutationVariables, ...>, which exposes the underlying { input: UpdateBountyInput } shape to callbacks; define wrapper-local option types that use the public variable shape (e.g., { id: string; data: Omit<UpdateBountyInput,"id"> }) for both useUpdateBounty's mutate/mutateAsync and useDeleteBounty's mutate, then intercept/adapt the options before forwarding to mutation.mutate/mutateAsync: wrap onSuccess/onError/onSettled callbacks to translate the variables parameter from the underlying { input: UpdateBountyInput } to the wrapper { id, data } (and vice‑versa when calling mutation) so runtime callback args match the public API while preserving underlying call behavior.
207-217: Use the generatedBountyStatusenum instead of the raw string literal for consistency and maintainability.The file currently uses
"IN_PROGRESS"as a string literal in bothmutateandmutateAsyncbodies. The generatedBountyStatusenum (withBountyStatus.InProgress = "IN_PROGRESS") is available and should be used instead. While the current code passes type-checking sinceUpdateBountyInput.statusaccepts any string, using the enum prevents typos, improves refactor-resilience, and aligns with generated code best practices.♻️ Suggested refactor
-import { +import { + BountyStatus, useCreateBountyMutation,- mutation.mutate({ input: { id, status: "IN_PROGRESS" } }, options), + mutation.mutate({ input: { id, status: BountyStatus.InProgress } }, options),- mutation.mutateAsync({ input: { id, status: "IN_PROGRESS" } }, options), + mutation.mutateAsync({ input: { id, status: BountyStatus.InProgress } }, options),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-bounty-mutations.ts` around lines 207 - 217, Replace the raw string literal "IN_PROGRESS" with the generated enum value BountyStatus.InProgress in both the mutate and mutateAsync calls inside the object that wraps mutation.mutate and mutation.mutateAsync; update the payloads ({ input: { id, status: ... } }) used by mutate and mutateAsync so they reference BountyStatus.InProgress (importing BountyStatus if not already) to ensure enum-based status usage across use-bounty-mutations.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@hooks/use-bounty-mutations.ts`:
- Around line 56-77: The optimistic cache update using
queryClient.getQueryData<BountyQuery> and queryClient.setQueryData<BountyQuery>
with the preserved wrapper around previous and bounty (using
bountyKeys.detail(id)) is correct and requires no code changes; keep the current
logic in the mutation (including the optimisticInput construction and updatedAt
override) so useBountyDetail will continue to see data?.bounty as expected.
- Around line 188-195: The cache shape mismatch has been resolved by wrapping
the updated bounty result in the expected object shape; keep the onSuccess
handler in the mutation (the queryClient.setQueryData<BountyQuery> call that
sets bountyKeys.detail(variables.input.id) to { bounty: data.updateBounty } and
the subsequent queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }))
as-is—no change needed.
---
Nitpick comments:
In `@hooks/use-bounty-mutations.ts`:
- Around line 61-65: The optimistic update currently removes both undefined and
null values when building optimisticInput in the optimistic update (see the
optimisticInput creation that filters Object.entries(variables.input)), which
prevents intentionally-cleared nullable fields from being represented; change
the filter to only exclude undefined (i.e., ([, value]) => value !== undefined)
so null values are preserved in optimisticInput and null-clearing is reflected
immediately in the UI.
- Around line 97-119: The current mutate/mutateAsync signatures (in
hooks/use-bounty-mutations.ts) accept public inputs ({ id, data }) but still
type options as MutateOptions<..., UpdateBountyMutationVariables, ...>, which
exposes the underlying { input: UpdateBountyInput } shape to callbacks; define
wrapper-local option types that use the public variable shape (e.g., { id:
string; data: Omit<UpdateBountyInput,"id"> }) for both useUpdateBounty's
mutate/mutateAsync and useDeleteBounty's mutate, then intercept/adapt the
options before forwarding to mutation.mutate/mutateAsync: wrap
onSuccess/onError/onSettled callbacks to translate the variables parameter from
the underlying { input: UpdateBountyInput } to the wrapper { id, data } (and
vice‑versa when calling mutation) so runtime callback args match the public API
while preserving underlying call behavior.
- Around line 207-217: Replace the raw string literal "IN_PROGRESS" with the
generated enum value BountyStatus.InProgress in both the mutate and mutateAsync
calls inside the object that wraps mutation.mutate and mutation.mutateAsync;
update the payloads ({ input: { id, status: ... } }) used by mutate and
mutateAsync so they reference BountyStatus.InProgress (importing BountyStatus if
not already) to ensure enum-based status usage across use-bounty-mutations.ts.
In `@hooks/use-infinite-bounties.ts`:
- Line 15: Remove the explicit generic on useInfiniteQuery so TanStack Query can
infer TPageParam as number (i.e., change the call to useInfiniteQuery(...)
instead of useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>), keep
initialPageParam: 1, and then remove the redundant pageParam as number casts
found in the fetch function and getNextPageParam logic (references:
useInfiniteQuery call, initialPageParam, and the occurrences of pageParam as
number). This will let the hook infer page param type correctly and eliminate
the unnecessary type assertions.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
hooks/use-bounty-mutations.tshooks/use-infinite-bounties.ts
…a-GraphQL-API-service-layer
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/query/query-keys.ts`:
- Around line 13-14: bountyKeys.details() is dead code because
bountyKeys.detail(id) uses useBountyQuery.getKey({ id }) (root "Bounty") so the
keys don't share a prefix; fix by either (A) remove the unused
bountyKeys.details() export and any unreachable types/usages (verify
BountyQueryKey still matches), or (B) change the key hierarchy so detail(id) is
a child of details() — i.e., make bountyKeys.detail = (id) =>
[...bountyKeys.details(), id] (and then update all useBountyQuery call sites to
pass queryKey: bountyKeys.detail(id)); choose one option and apply consistently
across bountyKeys, useBountyQuery call sites (e.g., in bounty-queries.ts), and
types.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
components/bounty-detail/bounty-detail-sidebar-cta.tsxlib/query/query-keys.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- components/bounty-detail/bounty-detail-sidebar-cta.tsx
closes #100
createdByfield to theBountyFieldsfragment.Summary by CodeRabbit
New Features
Refactor
Chores