Skip to content

Conversation

@HarshitVerma109
Copy link
Contributor

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:

  • Created graphql_cache.py utility to invalidate specific cache entries
  • Added cache invalidation to update_program and update_program_status mutations
  • Added cache invalidation to create_module and update_module mutations

Frontend Changes:

  • Added refetchQueries + awaitRefetchQueries: true to mutation calls
  • Added fetchPolicy: 'cache-and-network' to detail pages

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

Summary by CodeRabbit

  • New Features
    • Automatic cache invalidation and refetching to keep program and module data up to date after create/update actions
  • User Experience
    • Loading spinners now only show when there's no data yet, reducing flicker
    • Queries now fetch fresh data while still using cached results for faster loads
  • Tests
    • Test assertions relaxed to allow additional mutation metadata (keeps tests stable)

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds 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

Cohort / File(s) Summary
Backend Cache Invalidation Utilities
backend/apps/common/graphql_cache.py
New module providing invalidate_graphql_cache(field_name, field_args) -> bool, invalidate_program_cache(program_key), and invalidate_module_cache(program_key, module_key) that compute cache keys and delete cached resolver responses.
Backend Mutation Cache Integration
backend/apps/mentorship/api/internal/mutations/program.py, backend/apps/mentorship/api/internal/mutations/module.py
Import cache invalidation helpers and call them after create/update operations; preserve existing error handling while invalidating old/new keys when keys change.
Frontend: Query Fetch Policy & Loading Logic
frontend/src/app/mentorship/programs/[programKey]/page.tsx, frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx, frontend/src/app/my/mentorship/programs/[programKey]/page.tsx, frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx
Add fetchPolicy: 'cache-and-network' to detail queries and render loading spinner only when loading and no data is present (isLoading && !data).
Frontend: Mutation Refetch Strategies
frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx, frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx, frontend/src/app/my/mentorship/programs/[programKey]/modules/create/page.tsx, frontend/src/app/my/mentorship/programs/create/page.tsx
Replace/augment local cache updates with refetchQueries (e.g., GetProgramAndModulesDocument, GetMyProgramsDocument) and awaitRefetchQueries: true to force queries to refetch after mutations.
Frontend Tests
frontend/__tests__/unit/pages/CreateProgram.test.tsx
Relax test assertion to use expect.objectContaining(...), verifying core input fields and presence of awaitRefetchQueries: true and refetchQueries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • kasya
  • arkid15r
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: add GraphQL cache invalidation for programs and modules' clearly and specifically summarizes the main change, focusing on the core solution of adding cache invalidation utilities.
Description check ✅ Passed The description is well-related to the changeset, explaining the stale data problem, backend cache invalidation utility, frontend refetch strategy, and fetchPolicy changes.
Linked Issues check ✅ Passed The PR fully addresses all three objectives from #3348: backend cache invalidation utility implemented, frontend awaitRefetchQueries used to synchronize navigation, and cache-and-network fetch policy added to detail pages.
Out of Scope Changes check ✅ Passed All changes are in-scope and directly support the cache invalidation objectives; test file updates reflect the backend cache invalidation behavior and frontend refetch requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 refetchQueries to refresh the data after a successful status update. While the toast indicates success, the displayed status on the page won't update because program remains stale in local state.

Per the PR objectives, mutations should use refetchQueries with awaitRefetchQueries: true to 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 updateStatus function 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.

err in catch blocks has type unknown. Direct property access without a type guard could fail at runtime if the error isn't an Error object. 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 error is present but not a "not found" error (e.g., network failure) and module is 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 with notifyOnNetworkStatusChange: true, the loading boolean will be true during background refetches, which could cause the LoadingSpinner to briefly replace existing content.

Consider checking for cached data presence or using networkStatus to 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 && !data

This 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 duplicate isNotFound computation.

The same error.message?.toLowerCase().includes('not found') logic is computed both in the useEffect (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_cache would 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)

Copy link
Collaborator

@arkid15r arkid15r left a 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.

@arkid15r arkid15r marked this pull request as draft January 17, 2026 23:13
@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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-network and notifyOnNetworkStatusChange: true, when cached data exists:

  1. isLoading = true (background fetch), data = cached value, but program = null (useEffect hasn't run)
  2. Line 93: true && !cachedValue → false (continues)
  3. Line 95: !null && !true → false (continues)
  4. Line 106: program.status → TypeError

Unlike ModuleDetailsPage which derives programModule directly from data, this component stores it in state via useEffect, 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 program directly from data like 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.

@HarshitVerma109 HarshitVerma109 marked this pull request as ready for review January 18, 2026 02:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix Stale Data Persistence in Mentorship Portal (Cache Invalidation)

3 participants