-
-
Notifications
You must be signed in to change notification settings - Fork 427
fix: add GraphQL cache invalidation for programs and modules #3387
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?
fix: add GraphQL cache invalidation for programs and modules #3387
Conversation
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings. WalkthroughAdds backend utilities to invalidate GraphQL resolver caches and wires them into program/module mutations; frontend queries set fetchPolicy to "cache-and-network" and several mutations use refetchQueries with awaitRefetchQueries to ensure fresh data is loaded before navigation. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
frontend/src/app/my/mentorship/programs/[programKey]/page.tsx (1)
28-30: Missing refetch or state update after status mutation.The mutation doesn't include
refetchQueriesto refresh the data after a successful status update. While the toast indicates success, the displayed status on the page won't update becauseprogramremains stale in local state.Per the PR objectives, mutations should use
refetchQuerieswithawaitRefetchQueries: trueto ensure fresh data is fetched after writes.Suggested fix
const [updateProgram] = useMutation(UpdateProgramStatusDocument, { onError: handleAppError, + refetchQueries: [{ query: GetProgramAndModulesDocument, variables: { programKey } }], + awaitRefetchQueries: true, })Alternatively, you could optimistically update the local state in the
updateStatusfunction after a successful mutation:await updateProgram({ variables: { inputData: { key: program.key, name: program.name, status: newStatus, }, }, }) + + setProgram((prev) => (prev ? { ...prev, status: newStatus } : null))frontend/src/app/my/mentorship/programs/[programKey]/modules/create/page.tsx (1)
116-124: Add type guard or optional chaining for error handling.
errin catch blocks has typeunknown. Direct property access without a type guard could fail at runtime if the error isn't anErrorobject. The create program page uses safer optional chaining (err?.message).Suggested fix
} catch (err) { addToast({ title: 'Creation Failed', - description: err.message || 'Something went wrong while creating the module.', + description: err instanceof Error ? err.message : 'Something went wrong while creating the module.', color: 'danger', variant: 'solid', timeout: 4000, }) }frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx (1)
35-53: Error handling may misclassify network errors as "not found".When
erroris present but not a "not found" error (e.g., network failure) andmoduleis null (no cached data), the UI shows "Module Not Found" (404) which is misleading. Consider distinguishing between actual 404s and other errors without cached data.💡 Suggested improvement
if (isLoading) return <LoadingSpinner /> + if (error && !error.message?.toLowerCase().includes('not found')) { + return ( + <ErrorDisplay + statusCode={500} + title="Error Loading Module" + message="An error occurred while loading the module. Please try again." + /> + ) + } + if (!module) { return ( <ErrorDisplay statusCode={404} title="Module Not Found" message="Sorry, the module you're looking for doesn't exist." /> ) }
🤖 Fix all issues with AI agents
In `@frontend/__tests__/unit/pages/CreateProgram.test.tsx`:
- Around line 162-178: The test currently only asserts awaitRefetchQueries on
mockCreateProgram and misses asserting refetchQueries; update the assertion for
mockCreateProgram (the expect(mockCreateProgram).toHaveBeenCalledWith(...)
block) to also include a refetchQueries property with the expected query (or at
least an array) so the mutation call contains both awaitRefetchQueries: true and
refetchQueries: [/* expected query or queries */]; adjust the
expect.objectContaining to include refetchQueries alongside variables and
awaitRefetchQueries to lock in cache-refresh behavior.
In `@frontend/src/app/mentorship/programs/`[programKey]/page.tsx:
- Around line 20-24: The current useQuery call (GetProgramAndModulesDocument
with variables: { programKey } and fetchPolicy: 'cache-and-network')
unconditionally shows a spinner when loading is true, which hides cached data;
update the render logic that checks loading (the spinner at the current page
component) to only show the spinner when loading is true AND data is falsy
(e.g., if (!data && loading) show spinner), so cached data renders while the
network request proceeds; apply the same change to the analogous modules page
component that uses useQuery there.
- Around line 30-34: Extract the 404 detection into a reusable helper named
isNotFoundError(error) (place it in a shared module like utils/errorUtils or
lib/graphql/errorUtils) and replace the inline checks that use
graphQLRequestError.message?.toLowerCase().includes('not found') in page.tsx and
modules/[moduleKey]/page.tsx with if (isNotFoundError(graphQLRequestError)) {
... } else { handleAppError(graphQLRequestError) }; implement isNotFoundError to
first inspect error.graphQLErrors[].extensions.code for 'NOT_FOUND' or
'RESOURCE_NOT_FOUND', then check error.networkError?.statusCode/response status
for 404, and finally fall back to a case-insensitive message substring match to
preserve current behavior. Ensure the helper is exported and imported where used
so both files share the same logic.
🧹 Nitpick comments (4)
frontend/src/app/my/mentorship/programs/[programKey]/page.tsx (1)
32-37: Consider refining the loading state to avoid UI flicker during background refetch.The
fetchPolicy: 'cache-and-network'addition aligns well with the PR's cache invalidation objectives. However, combined withnotifyOnNetworkStatusChange: true, theloadingboolean will betrueduring background refetches, which could cause theLoadingSpinnerto briefly replace existing content.Consider checking for cached data presence or using
networkStatusto distinguish initial load from background refetch:Suggested improvement
- const { data, loading: isQueryLoading } = useQuery(GetProgramAndModulesDocument, { + const { data, loading: isQueryLoading, networkStatus } = useQuery(GetProgramAndModulesDocument, { variables: { programKey }, skip: !programKey, fetchPolicy: 'cache-and-network', notifyOnNetworkStatusChange: true, }) - const isLoading = isQueryLoading + // Only show loading spinner on initial load, not background refetch + const isLoading = isQueryLoading && !dataThis ensures the spinner only appears when there's no cached data to display, preventing flicker during background network requests.
frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx (1)
32-39: Consider extracting duplicateisNotFoundcomputation.The same
error.message?.toLowerCase().includes('not found')logic is computed both in theuseEffect(line 34) and in the render block (line 44). While the overhead is negligible, extracting this to a single derived value improves maintainability.♻️ Suggested refactor
+ const isNotFound = error?.message?.toLowerCase().includes('not found') + useEffect(() => { if (error) { - const isNotFound = error.message?.toLowerCase().includes('not found') if (!isNotFound) { handleAppError(error) } } - }, [error]) + }, [error, isNotFound]) if (isLoading) return <LoadingSpinner /> if (error) { - const isNotFound = error.message?.toLowerCase().includes('not found') return ( <ErrorDisplay statusCode={isNotFound ? 404 : 500}Also applies to: 43-56
backend/apps/common/graphql_cache.py (2)
10-10: Consider adding debug logging for cache invalidation.The logger is imported but unused. Adding debug-level logging in
invalidate_graphql_cachewould help diagnose cache issues in production.♻️ Optional: Add debug logging
def invalidate_graphql_cache(field_name: str, field_args: dict) -> bool: ... key = f"{field_name}:{json.dumps(field_args, sort_keys=True)}" cache_key = f"{settings.GRAPHQL_RESOLVER_CACHE_PREFIX}-{hashlib.sha256(key.encode()).hexdigest()}" - result = cache.delete(cache_key) - return result + deleted = cache.delete(cache_key) + logger.debug("Cache invalidation for %s: %s", field_name, "hit" if deleted else "miss") + return deleted
25-26: Minor: Simplify return statement.Per static analysis (RET504), the intermediate assignment is unnecessary.
♻️ Suggested simplification
- result = cache.delete(cache_key) - return result + return cache.delete(cache_key)
arkid15r
left a comment
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.
The CR's comments are unaddressed.
|
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.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/app/my/mentorship/programs/[programKey]/page.tsx (1)
93-114: Potential null reference error due to state sync timing.With
cache-and-networkandnotifyOnNetworkStatusChange: true, when cached data exists:
isLoading= true (background fetch),data= cached value, butprogram= null (useEffect hasn't run)- Line 93:
true && !cachedValue→ false (continues)- Line 95:
!null && !true→ false (continues)- Line 106:
program.status→ TypeErrorUnlike
ModuleDetailsPagewhich derivesprogramModuledirectly fromdata, this component stores it in state viauseEffect, creating a timing gap.🔧 Suggested fix: Guard against null program before accessing properties
- if (isLoading && !data) return <LoadingSpinner /> + if (isLoading && !program) return <LoadingSpinner /> if (!program && !isLoading) {Alternatively, derive
programdirectly fromdatalike the other page does:- const [program, setProgram] = useState<Program | null>(null) - const [modules, setModules] = useState<Module[]>([]) ... - useEffect(() => { - if (data?.getProgram) { - setProgram(data.getProgram) - setModules(data.getProgramModules || []) - } - }, [data]) + const program = data?.getProgram ?? null + const modules = data?.getProgramModules ?? []This eliminates the race condition by deriving state synchronously from
data.



Proposed change
Resolves #3348
This PR fixes the mentorship portal cache invalidation issue where updates to programs and modules were not visible immediately after saving. The backend GraphQL resolver cache was not being cleared after mutations, causing stale data to be served.
Backend Changes:
graphql_cache.pyutility to invalidate specific cache entriesFrontend Changes:
Checklist
make check-testlocally: all warnings addressed, tests passed