Skip to content

Conversation

@drifter089
Copy link
Owner

@drifter089 drifter089 commented Jan 10, 2026

Summary

Fixes role data disappearing after creation/update/delete by removing unnecessary TanStack Query cache invalidations that could overwrite correct data with stale Prisma Accelerate responses.

Problem

After role mutations, calling invalidate() triggered refetches that could return stale data from Prisma Accelerate cache, overwriting the correct data we already set via setData().

Solution

  • Remove invalidate() calls for role.getByTeamId after mutations
  • Keep only team.getById.invalidate() for role count updates
  • Trust setData() with server response as authoritative data
  • Server already invalidates Prisma cache before returning

Changes

  • use-create-role.tsx - Remove role cache invalidation
  • use-delete-role.tsx - Change from onSettled to onSuccess, remove role cache invalidation
  • role-card.tsx - Change from onSettled to onSuccess, remove role/dashboard cache invalidation
  • use-optimistic-role-update.ts - Remove delayed (5s) cache invalidation

Summary by CodeRabbit

  • Refactor
    • Simplified cache/sync behavior for role create/delete by removing automatic background and delayed invalidations, relying on immediate optimistic updates instead.
    • Reduced redundant delayed refreshes to lower network activity and perceived lag.
    • Expanded cache invalidation for metric deletions to also clear related dashboard and role caches for more consistent state.

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

@vercel
Copy link

vercel bot commented Jan 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
org-os Ready Ready Preview, Comment Jan 10, 2026 4:36pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

📝 Walkthrough

Walkthrough

Removed several client-side cache invalidations from role create/delete and optimistic-update flows, eliminated a delayed 5s invalidation, and added tag-based cache invalidation for team-scoped entries in the metric delete mutation. No exported API signatures changed.

Changes

Cohort / File(s) Summary
Role mutation hooks & components
src/app/teams/[teamId]/hooks/use-create-role.tsx, src/app/teams/[teamId]/hooks/use-delete-role.tsx, src/components/role/role-card.tsx, src/hooks/use-optimistic-role-update.ts
Removed client-side invalidation calls (e.g., utils.role.getByTeamId.invalidate) and removed onSettled handlers; eliminated the 5s delayed post-success invalidation in optimistic-update flow; retained immediate optimistic setData updates and removed background invalidation logic.
Server metric cache invalidation
src/server/api/routers/metric.ts
Replaced single helper import with invalidateCacheByTags + invalidateDashboardCache; after metric deletion now invalidates dashboard plus team-tagged cache entries via invalidateCacheByTags('team_<teamId>').

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through code at break of dawn,

setData quick, no timeout yawn.
Tags cleared gently, caches prune,
I munched the bugs and left a tune.
Hoppity fixes—off I zoom! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: removing TanStack Query cache invalidation calls to prevent stale data overwrites, which is the core problem and solution across all modified files.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@claude
Copy link
Contributor

claude bot commented Jan 10, 2026

PR Review: Fix TanStack Cache Invalidation

Summary

This PR addresses a critical caching issue where role data was disappearing after mutations due to race conditions between TanStack Query invalidate calls and Prisma Accelerate cache propagation.

Strengths

  1. Root Cause Analysis: Correctly identifies that calling invalidate after setData can trigger refetches returning stale Prisma Accelerate data
  2. Consistent Pattern: Applied uniformly across all role mutation hooks
  3. Server-Side Cache Invalidation: All mutations properly invalidate Prisma Accelerate cache tags
  4. Clear Documentation: Inline comments explain the reasoning
  5. Lifecycle Hook Correction: Changed from onSettled to onSuccess in delete operations

Concerns & Questions

1. Prisma Accelerate Cache Propagation Timing

  • Prisma Accelerate cache invalidation is asynchronous with potential delays
  • listCache strategy has ttl: 60s, swr: 120s
  • Rapid mutations may still hit stale cache
  • Question: Has this been tested with rapid successive mutations within 5 seconds?

2. Dashboard Cache Not Invalidated

  • Dashboard cache invalidation removed from use-optimistic-role-update.ts and role-card.tsx
  • Issue: When roles updated from role card, dashboard won't reflect changes until cache expires (up to 6 minutes)
  • Recommendation: Consider keeping dashboard.getDashboardCharts.invalidate for scenarios where dashboard cache isn't updated via setData

3. Missing Background Refresh Mechanism

  • Old delayed invalidation (5s timeout) served as safety net for eventual consistency
  • Without it, if Prisma Accelerate invalidation fails, client will have stale data
  • Recommendation: Add lower-priority background invalidation with 30-60s delay

4. Team Role Count Update

  • Question: Does team.getById query use Prisma Accelerate caching with tags?

Security Considerations

  • Authorization checks remain intact
  • No exposure of sensitive data
  • Optimistic updates properly rollback on error

Test Coverage

Missing E2E/integration tests. Recommended:

  1. Rapid Mutation Test: Create, Update, Delete roles within 5 seconds
  2. Multi-Tab Test: Mutate in one tab, verify other tabs see changes
  3. Dashboard Sync Test: Update role-metric assignment, verify dashboard reflects change

Performance Considerations

Positive: Removes unnecessary network requests, reduces server load
Concern: Dashboard may show stale data for up to 6 minutes
Recommendation: Consider reducing dashboardCache.swr from 300s to 60s

Recommendations

High Priority:

  1. Add E2E test for rapid mutations
  2. Restore dashboard.getDashboardCharts.invalidate in role-card.tsx or document why not needed
  3. Document expected propagation time for Prisma Accelerate invalidation

Medium Priority:
4. Add background invalidation safety mechanism
5. Consider reducing dashboardCache.swr to 60s
6. Add monitoring for cache invalidation failures

Approval Recommendation

Approve with minor revisions. Core fix is sound but recommend addressing dashboard cache invalidation concern (#2) before merging to avoid confusing UX where changes don't appear on dashboard for several minutes.

Great work identifying this tricky caching issue! The pattern of trusting server responses via setData over background invalidation is the right approach.

@claude
Copy link
Contributor

claude bot commented Jan 10, 2026

Pull Request Review

Summary

This PR removes TanStack Query cache invalidations after role mutations to prevent stale data from overwriting correct data. The changes align well with the project's documented caching strategy.


✅ Strengths

1. Problem Correctly Identified
The issue description accurately captures the race condition: calling invalidate() can trigger refetches that return stale data from Prisma Accelerate cache before server-side cache invalidation propagates across all regions.

2. Aligns with Documented Architecture
The changes follow the pattern described in CLAUDE.md:208-213:

Why setData before invalidate? Prisma Accelerate cache may not propagate immediately. If we only call invalidate(), the refetch might return stale data. Setting cache with server response ensures correct data.

3. Server-Side Cache Invalidation is Robust
Verified that all role mutations (create, update, delete) properly invalidate Prisma Accelerate cache:

  • role.ts:117-121 - create
  • role.ts:208-212 - update
  • role.ts:229-233 - delete

This means the server already handles cache consistency, making client-side invalidation redundant.

4. Consistent Pattern Application
All four modified files follow the same pattern:

  • Keep setData() with server response in onSuccess
  • Remove invalidate() for role/dashboard queries
  • Retain only team.getById.invalidate() for role count updates

🔍 Observations & Considerations

1. Change from onSettled to onSuccess

-    onSettled: () => {
+    onSuccess: () => {

Analysis: This is correct. onSettled runs on both success AND error, which could invalidate caches even when mutations fail. Using onSuccess ensures cache invalidation only happens after successful mutations.

2. Removal of Dashboard Cache Invalidation (role-card.tsx:143)

-        void utils.dashboard.getDashboardCharts.invalidate({ teamId });

Question: The RoleCard component can update role properties that appear in dashboard charts (title, color, assignedUserId, effortPoints). The useOptimisticRoleUpdate hook already updates the dashboard cache via setData() in onSuccess. However, does role-card.tsx use useOptimisticRoleUpdate or its own mutation?

Found: role-card.tsx:124 uses api.role.delete.useMutation directly, NOT useDeleteRole or useOptimisticRoleUpdate. This means it doesn't benefit from the optimistic update logic. However, since role deletion removes the role entirely, there's no dashboard data to update beyond removal, which is handled by cache removal on line 131.

Verdict: ✅ Safe. Dashboard invalidation removal is correct here.

3. 5-Second Delayed Invalidation Removal

-      // Delayed invalidation for eventual consistency
-      // Wait for Prisma Accelerate cache propagation before background refresh
-      setTimeout(() => {
-        void utils.role.getByTeamId.invalidate({ teamId });
-        void utils.dashboard.getDashboardCharts.invalidate({ teamId });
-      }, 5000);

Analysis: This was a workaround for the exact problem being fixed. The 5-second delay was attempting to wait for Prisma cache propagation, but:

  • It's arbitrary and could still fail
  • It causes unnecessary refetches
  • The server-side cache invalidation handles this properly

Verdict: ✅ Excellent cleanup.


🎯 Code Quality

Consistency: ✅ All changes follow the same pattern
Best Practices: ✅ Uses onSuccess appropriately instead of onSettled
Documentation: ✅ Comments updated to remove misleading "background refresh" mentions
Type Safety: ✅ No type changes, all existing types respected


🔒 Potential Issues

1. Edge Case: Rapid Successive Mutations

If a user performs multiple role updates in quick succession (e.g., editing multiple fields in rapid succession), there's a theoretical risk that:

  1. First mutation completes, calls setData()
  2. Second mutation starts before first mutation's Prisma cache invalidation propagates globally
  3. Second mutation might read stale base data

Mitigation: This is already handled by:

  • Optimistic updates (onMutate) ensure immediate UI feedback
  • Each mutation's setData() uses the server response as truth
  • Server-side cache invalidation happens synchronously before response

Verdict: ✅ Low risk, properly mitigated.

2. Multi-Tab/Multi-User Scenarios

Without client-side invalidation, changes made in one tab/by another user won't appear until:

  • User manually refreshes
  • User navigates away and back
  • A new mutation triggers team.getById.invalidate() (which is kept)

Current Behavior: The team.getById.invalidate() call will trigger a refetch that includes role count, but NOT role details. This means role count stays in sync, but role data might be stale across tabs.

Mitigation Options:

  • Use WebSockets/polling for multi-user collaboration (not in scope)
  • Add periodic background refetch via refetchInterval on queries
  • Document this as known behavior

Verdict: ⚠️ Known trade-off. Document if this becomes a user-reported issue.


🧪 Testing Recommendations

Manual Testing Checklist:

  • Create role → verify appears immediately → refresh page → verify persists
  • Update role (title, color, metric) → verify UI updates → refresh → verify persists
  • Delete role → verify disappears → refresh → verify stays deleted
  • Assign metric to role → verify appears on dashboard → refresh → verify persists
  • Two tabs open → edit role in Tab 1 → verify Tab 2 doesn't show change until explicit refresh (expected behavior)

Edge Cases:

  • Create role, immediately edit it
  • Rapid successive updates to same role
  • Network latency simulation (slow 3G)

📊 Performance Impact

Before: Every mutation triggered 1-3 background refetches (role + dashboard + team)
After: Only team query refetches (for role count)

Estimated Impact:

  • ~66% reduction in post-mutation network requests
  • Faster perceived performance (no loading states after mutations)
  • Lower server load on high-traffic teams

🎬 Recommendation

Approve with minor suggestion:

Consider adding a comment in one of the hooks explaining why invalidation is removed, to prevent future developers from re-adding it:

onSuccess: (updatedRole) => {
  // Update cache with server response (authoritative data)
  utils.role.getByTeamId.setData({ teamId }, (old) => {
    if (!old) return [updatedRole];
    return old.map((role) =>
      role.id === updatedRole.id ? updatedRole : role,
    );
  });

  // No invalidate() call here - server already invalidated Prisma cache.
  // Calling invalidate() could refetch stale data before cache propagation.
  
  // Only invalidate team for role count updates
  void utils.team.getById.invalidate({ id: teamId });
},

This is optional but would help maintain the fix long-term.


Final Verdict: ✅ LGTM - Approve

This PR correctly addresses a real caching issue with a clean, consistent solution. The changes are safe, well-reasoned, and improve both correctness and performance.

Copy link

@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

🧹 Nitpick comments (1)
src/app/teams/[teamId]/hooks/use-delete-role.tsx (1)

82-84: Document the intentional removal of role.getByTeamId.invalidate() (prevents regression vs repo guidance).

This onSuccess behavior conflicts with existing repo guidance that says “setData(...) then invalidate() for background refresh” (from retrieved learnings). Since this PR intentionally does not invalidate to avoid Prisma Accelerate stale overwrites, please add a short inline comment (or update the shared guidance doc) explaining the rationale so someone doesn’t reintroduce invalidate() later.

Proposed patch (inline comment)
   onSuccess: () => {
+    // Intentionally do NOT invalidate `role.getByTeamId` here:
+    // immediate refetch may hit Prisma Accelerate stale cache and overwrite our optimistic `setData`.
+    // We rely on optimistic cache + server-side cache invalidation; keep only team refetch for role counts.
     void utils.team.getById.invalidate({ id: teamId });
   },
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7bbc5b and 90d7990.

📒 Files selected for processing (4)
  • src/app/teams/[teamId]/hooks/use-create-role.tsx
  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
  • src/components/role/role-card.tsx
  • src/hooks/use-optimistic-role-update.ts
💤 Files with no reviewable changes (2)
  • src/app/teams/[teamId]/hooks/use-create-role.tsx
  • src/hooks/use-optimistic-role-update.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/role/role-card.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (GEMINI.md)

Use TypeScript 5.9 with strict type checking for all frontend and backend code

Files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
src/**/*.tsx

📄 CodeRabbit inference engine (GEMINI.md)

Prefer Server Components for initial data fetching; use Client Components ('use client') only for interactivity

Files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
src/**/*/*.tsx

📄 CodeRabbit inference engine (GEMINI.md)

Client Components must use import { api } from '@/trpc/react' for standard HTTP/Hooks wrapper

Files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
**/*.tsx

📄 CodeRabbit inference engine (GEMINI.md)

Use Tailwind CSS 4 for styling with shadcn/ui and Radix UI primitive components

Files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
src/app/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

src/app/**/*.tsx: Use the dual tRPC API pattern: direct calls in Server Components (api.team.getById) for 10x faster performance, and React hooks in Client Components (api.team.getById.useQuery)
Use getUserDisplayName(userId, members) utility (client-side sync) from @/lib/helpers/get-user-name for displaying user names in components

Files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use @trivago/prettier-plugin-sort-imports with inline type imports for import organization

Files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/**/*role*mutation*.{ts,tsx} : For role mutations, use cache pipeline with two operations: (1) onMutate for optimistic update, (2) onSuccess with both setData(updatedRole) using server response AND invalidate() for background refresh. Always use server response data in setData before invalidate to ensure Prisma Accelerate cache consistency.
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/hooks/**/*.ts : Use optimistic updates with setData before invalidate in TanStack Query mutations to ensure correct data when Prisma Accelerate cache hasn't propagated
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/dashboard/[teamId]/**/*.{ts,tsx} : Dashboard cache updates for role-metric assignments must update both `role.getByTeamId` and `dashboard.getDashboardCharts` caches during mutations. Use onMutate for optimistic updates on both caches, then invalidate both on success.
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/hooks/use-optimistic-role-update.ts : The `use-optimistic-role-update` hook provides shared logic for all role mutations with proper cache layer handling for TanStack Query and Prisma Accelerate. Use this hook in canvas-specific wrappers to add markDirty side effects.
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/hooks/**/*.ts : For role-metric cache updates, optimistically update both role cache (role.getByTeamId) and dashboard cache (dashboard.getDashboardCharts) to maintain UI consistency
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/app/teams/[teamId]/_components/**/*.tsx : Implement cache-first node pattern for role nodes: store only roleId in node data, fetch display data from TanStack Query cache using useRoleData hook
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/teams/[teamId]/**/*.{ts,tsx} : Cache-first node system: Role nodes store only `roleId` in node.data. Display data must be fetched from TanStack Query cache using the `useRoleData()` hook which queries the `role.getByTeamId` procedure.
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-12-20T22:12:00.576Z
Learning: Applies to src/app/teams/[teamId]/**/*.tsx : React Flow nodes must store minimal data (e.g., just `roleId`); fetch full Role data from TanStack Query cache in the Node component to keep canvas and sidebars in sync
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/server/api/routers/**/*.ts : Invalidate dashboard cache after mutations using invalidateDashboardCache(ctx.db, organizationId, teamId)
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/server/api/routers/**/*.ts : After mutations, invalidate cache tags using `invalidateCacheByTags(ctx.db, [``tag_${id}``])` to trigger background cache updates with Prisma Accelerate.
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/**/*role*mutation*.{ts,tsx} : For role mutations, use cache pipeline with two operations: (1) onMutate for optimistic update, (2) onSuccess with both setData(updatedRole) using server response AND invalidate() for background refresh. Always use server response data in setData before invalidate to ensure Prisma Accelerate cache consistency.

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/hooks/use-optimistic-role-update.ts : The `use-optimistic-role-update` hook provides shared logic for all role mutations with proper cache layer handling for TanStack Query and Prisma Accelerate. Use this hook in canvas-specific wrappers to add markDirty side effects.

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/dashboard/[teamId]/**/*.{ts,tsx} : Dashboard cache updates for role-metric assignments must update both `role.getByTeamId` and `dashboard.getDashboardCharts` caches during mutations. Use onMutate for optimistic updates on both caches, then invalidate both on success.

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/app/teams/[teamId]/_components/**/*.tsx : Implement cache-first node pattern for role nodes: store only roleId in node data, fetch display data from TanStack Query cache using useRoleData hook

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/teams/[teamId]/**/*.{ts,tsx} : Cache-first node system: Role nodes store only `roleId` in node.data. Display data must be fetched from TanStack Query cache using the `useRoleData()` hook which queries the `role.getByTeamId` procedure.

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-20T22:12:00.576Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-12-20T22:12:00.576Z
Learning: Applies to src/app/teams/[teamId]/**/*.tsx : React Flow nodes must store minimal data (e.g., just `roleId`); fetch full Role data from TanStack Query cache in the Node component to keep canvas and sidebars in sync

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/hooks/**/*.ts : For role-metric cache updates, optimistically update both role cache (role.getByTeamId) and dashboard cache (dashboard.getDashboardCharts) to maintain UI consistency

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-20T22:12:00.576Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-12-20T22:12:00.576Z
Learning: Applies to src/app/teams/[teamId]/**/*.tsx : Avoid modifying `enrichNodesWithRoleData` flow without understanding the complete canvas serialization logic for saving/loading React Flow nodes to the database

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/hooks/**/*.ts : Use optimistic updates with setData before invalidate in TanStack Query mutations to ensure correct data when Prisma Accelerate cache hasn't propagated

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/teams/[teamId]/_components/role-node.tsx,src/app/teams/[teamId]/_components/public-role-node.tsx : These role node components are 75% identical and should be consolidated. Extract shared `RoleNodeTemplate` component with `isEditable` prop to DRY up the code.

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/teams/[teamId]/**/*.{ts,tsx} : Canvas auto-save system: Changes trigger markDirty() → Debounce 2s → serializeNodes/Edges → tRPC mutation. Include beforeunload sendBeacon fallback for unsaved changes.

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/app/teams/[teamId]/_components/**/*.tsx : Auto-save canvas changes with 2-second debounce: markDirty() → debounce → serializeNodes/Edges → tRPC mutation, with beforeunload sendBeacon fallback

Applied to files:

  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review

@claude
Copy link
Contributor

claude bot commented Jan 10, 2026

PR Review: Fix TanStack Cache Invalidation to Prevent Stale Data

Overview

This PR addresses a critical cache consistency issue where role data would disappear after mutations due to stale data from Prisma Accelerate overwriting correct data in TanStack Query cache.


✅ Strengths

1. Correct Problem Identification

The root cause analysis is spot-on. The dual-cache architecture (TanStack Query + Prisma Accelerate) can cause race conditions when:

  • Client calls setData() with server response (correct data)
  • Client calls invalidate() which triggers refetch
  • Refetch hits stale Prisma Accelerate cache
  • Stale data overwrites correct data

2. Clean Solution

Removing client-side invalidate() calls is the right approach because:

  • Server already invalidates Prisma cache via invalidateCacheByTags() (role.ts:118-121, 209-212, 230-233)
  • setData() with server response provides immediate, correct data
  • Background refetch is unnecessary when server guarantees cache invalidation

3. Consistent Pattern

All role mutation hooks follow the same optimistic update pattern:

  • onMutate: Optimistic update
  • onSuccess: setData() with server response (no invalidate)
  • onError: Rollback to previous data

4. Metric Delete Enhancement

The addition of team_{teamId} cache tag invalidation in metric.ts:279 is excellent - ensures role cache is cleared when metrics are deleted (roles have metricId set to null on cascade).


⚠️ Concerns & Questions

1. Missing Team Cache Invalidation in role-card.tsx

The deleteRoleMutation in role-card.tsx (lines 124-140) removes onSettled but doesn't add onSuccess:

const deleteRoleMutation = api.role.delete.useMutation({
  onMutate: async () => {
    // ... optimistic update
  },
  onError: (_error, _variables, context) => {
    // ... rollback
  },
  // ❌ Missing onSuccess to update cache with server response
});

Issue: After successful deletion, TanStack cache still has stale role. While the optimistic update removes it, if the mutation fails and rolls back, then succeeds on retry, the cache won't be updated.

Recommendation: Add onSuccess callback:

onSuccess: () => {
  // Role already removed in onMutate, but confirm server deletion
  if (teamId) {
    utils.role.getByTeamId.setData({ teamId }, (old) => {
      if (\!old) return [];
      return old.filter((r) => r.id \!== roleIdResolved);
    });
  }
},

2. Team Count Cache Still Using Invalidate

Comment in use-create-role.tsx mentions keeping team.getById.invalidate() for role count updates, but it was removed (line 225-226). This is actually good because:

  • Server invalidates team cache via team_{teamId} tag
  • Client doesn't need background refresh

However, team.getById query needs cache tags for this to work. Please verify:

// In team router
getById: workspaceProcedure
  .query(async ({ ctx, input }) => {
    return ctx.db.team.findUnique({
      where: { id: input.id },
      ...cacheStrategyWithTags(singleItemCache, [`team_{input.id}`])
    });
  })

3. Dashboard Cache Propagation

In use-delete-role.tsx, the hook doesn't update dashboard cache during deletion. While the server invalidates dashboard_team_{teamId} tag, the client cache won't reflect the removal until next refetch.

Current behavior:

  1. Delete role → optimistic update removes role from role.getByTeamId cache
  2. Dashboard chart still shows role in metric.roles array (stale)
  3. User navigates to dashboard → sees ghost role until cache expires

Recommendation: Add dashboard cache update in onMutate:

onMutate: async (variables) => {
  // ... existing code
  
  // Update dashboard cache
  utils.dashboard.getDashboardCharts.setData({ teamId }, (old) => {
    if (\!old) return old;
    return old.map(chart => ({
      ...chart,
      metric: {
        ...chart.metric,
        roles: chart.metric.roles.filter(r => r.id \!== variables.id)
      }
    }));
  });
}

4. No Test Coverage

No E2E or integration tests verify the fix. Given this is a critical bug affecting data consistency, tests would prevent regression.

Recommendation: Add Playwright test:

test('role persists after creation', async ({ authenticatedPage }) => {
  // Create role
  // Verify role appears in canvas
  // Wait 2 seconds (beyond cache TTL)
  // Verify role still exists
  // Refresh page
  // Verify role still exists
});

5. Potential Race Condition Window

Between mutation completion and server-side cache invalidation, there's a small window where:

  1. Client A updates role → setData() updates cache
  2. Client B fetches roles → gets stale data from Prisma cache (not yet invalidated)
  3. Server completes cache invalidation
  4. Client B's stale data is now in TanStack cache

Impact: Low (requires concurrent users on same team)
Mitigation: Already handled by server-side invalidation, but consider optimistic locking if this becomes an issue.


🔍 Code Quality

Positive:

  • Clean diff removing unnecessary code ✅
  • Follows CLAUDE.md cache pipeline guidance ✅
  • Consistent with documented patterns ✅
  • No over-engineering ✅

Minor Issues:

  • use-delete-role.tsx has inconsistent onError vs onSettled pattern with other hooks (missing onSuccess)
  • PR description could mention metric.ts change (easy to miss)

🚀 Performance Impact

Before:

  • Every mutation → 2 network requests (mutation + invalidate refetch)
  • Race conditions → multiple re-renders as stale data arrives

After:

  • Every mutation → 1 network request (mutation only)
  • Immediate UI updates with correct server data
  • Estimated 50% reduction in role mutation latency

🔐 Security Considerations

No security concerns. Changes only affect client-side caching behavior.


📋 Recommendations

Critical:

  1. Add onSuccess to role-card.tsx delete mutation (prevents edge case stale data)
  2. Update dashboard cache in use-delete-role.tsx (prevents ghost roles on dashboard)

Important:

  1. 🟡 Verify team.getById has cache tags (confirm server invalidation works)
  2. 🟡 Add E2E test (prevent regression)

Optional:

  1. 🔵 Document dual-cache strategy in CLAUDE.md (this PR reveals important patterns)

Verdict

APPROVE with minor changes

The core fix is correct and addresses the root cause. The removal of client-side invalidation is the right architectural decision. The metric.ts enhancement is a nice catch.

Address the role-card.tsx and use-delete-role.tsx dashboard cache concerns before merging to ensure complete cache consistency.

Great debugging work on identifying this tricky dual-cache interaction! 🎉


Files Changed Summary

File Change Impact
use-create-role.tsx Remove invalidate calls ✅ Correct
use-delete-role.tsx Remove onSettled invalidate ⚠️ Needs dashboard update
role-card.tsx Remove onSettled invalidate ⚠️ Needs onSuccess
use-optimistic-role-update.ts Remove 5s delayed invalidate ✅ Correct
metric.ts Add team cache invalidation ✅ Excellent

Copy link

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/api/routers/metric.ts (1)

252-282: Blocker: delete mutation is missing org/access verification before deleting.

ctx.db.metric.findUnique({ where: { id } }) + metric.delete({ where: { id } }) allows deleting any metric by ID without ensuring it belongs to ctx.workspace.organizationId (and, by extension, the current user/workspace). As per coding guidelines, use getMetricAndVerifyAccess (or equivalent) here.

Also, metric.teamId can be null; invalidateCacheByTags(ctx.db, [\team_${metric.teamId}`])will emitteam_null` and may waste invalidation budget—guard it.

Proposed fix
   delete: workspaceProcedure
     .input(z.object({ id: z.string() }))
     .mutation(async ({ ctx, input }) => {
-      // Get metric info before deletion for cache invalidation
-      const metric = await ctx.db.metric.findUnique({
-        where: { id: input.id },
-        select: { teamId: true, organizationId: true },
-      });
-
-      if (!metric) {
-        throw new TRPCError({
-          code: "NOT_FOUND",
-          message: "Metric not found",
-        });
-      }
+      // Verify metric belongs to user's organization (and fetch for cache invalidation)
+      const metric = await getMetricAndVerifyAccess(
+        ctx.db,
+        input.id,
+        ctx.workspace.organizationId,
+      );

       // Roles with this metric will have metricId set to null (onDelete: SetNull)
       await ctx.db.metric.delete({
         where: { id: input.id },
       });

       // Invalidate Prisma cache for dashboard and role queries
       await invalidateDashboardCache(
         ctx.db,
         metric.organizationId,
         metric.teamId,
       );
-      await invalidateCacheByTags(ctx.db, [`team_${metric.teamId}`]);
+      if (metric.teamId) {
+        await invalidateCacheByTags(ctx.db, [`team_${metric.teamId}`]);
+      }

       return { success: true };
     }),
🤖 Fix all issues with AI agents
In @src/server/api/routers/metric.ts:
- Around line 12-15: The delete mutation in src/server/api/routers/metric.ts
must perform the same authorization as the update path: call
getMetricAndVerifyAccess(...) to verify the current user/team can delete the
metric before proceeding (mirror the update mutation's check and error
handling). After successful deletion, change the cache invalidation so it
doesn't emit a tag like team_null: only include the team tag when metric.teamId
is non-null (e.g., push/invalidate `team_${teamId}` only if teamId exists) and
always include org-level tags as done elsewhere; update the calls to
invalidateCacheByTags and invalidateDashboardCache accordingly so org-level
metrics do not generate a team_null tag.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90d7990 and 81bacdf.

📒 Files selected for processing (5)
  • src/app/teams/[teamId]/hooks/use-create-role.tsx
  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
  • src/components/role/role-card.tsx
  • src/hooks/use-optimistic-role-update.ts
  • src/server/api/routers/metric.ts
💤 Files with no reviewable changes (4)
  • src/components/role/role-card.tsx
  • src/app/teams/[teamId]/hooks/use-delete-role.tsx
  • src/app/teams/[teamId]/hooks/use-create-role.tsx
  • src/hooks/use-optimistic-role-update.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (GEMINI.md)

Use TypeScript 5.9 with strict type checking for all frontend and backend code

Files:

  • src/server/api/routers/metric.ts
src/server/api/routers/**/*.ts

📄 CodeRabbit inference engine (GEMINI.md)

src/server/api/routers/**/*.ts: In tRPC procedures, never assume authentication; always use helpers like getTeamAndVerifyAccess to verify access before operations
Be careful when modifying team save logic related to EditSession model, which handles locking for concurrent team editing

src/server/api/routers/**/*.ts: Use tRPC protectedProcedure or workspaceProcedure for API routes; avoid manual auth checks in components as WorkOS middleware validates ctx.user
Always verify resources belong to user's organization using authorization helpers (getMetricAndVerifyAccess, getRoleAndVerifyAccess, getTeamAndVerifyAccess) in tRPC procedures
Invalidate dashboard cache after mutations using invalidateDashboardCache(ctx.db, organizationId, teamId)

Files:

  • src/server/api/routers/metric.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use @trivago/prettier-plugin-sort-imports with inline type imports for import organization

Files:

  • src/server/api/routers/metric.ts
src/server/api/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

src/server/api/**/*.ts: Use fetchUserDisplayName(userId) utility (server-side async WorkOS) from @/server/api/utils/get-user-display-name for server-side user name retrieval
Use enrichRolesWithUserNames() for flat role arrays and enrichChartRolesWithUserNames() for nested chart roles from src/server/api/utils/organization-members.ts

Files:

  • src/server/api/routers/metric.ts
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/**/*role*mutation*.{ts,tsx} : For role mutations, use cache pipeline with two operations: (1) onMutate for optimistic update, (2) onSuccess with both setData(updatedRole) using server response AND invalidate() for background refresh. Always use server response data in setData before invalidate to ensure Prisma Accelerate cache consistency.
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/hooks/**/*.ts : Use optimistic updates with setData before invalidate in TanStack Query mutations to ensure correct data when Prisma Accelerate cache hasn't propagated
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/app/teams/[teamId]/_components/**/*.tsx : Implement cache-first node pattern for role nodes: store only roleId in node data, fetch display data from TanStack Query cache using useRoleData hook
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/dashboard/[teamId]/**/*.{ts,tsx} : Dashboard cache updates for role-metric assignments must update both `role.getByTeamId` and `dashboard.getDashboardCharts` caches during mutations. Use onMutate for optimistic updates on both caches, then invalidate both on success.
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/hooks/use-optimistic-role-update.ts : The `use-optimistic-role-update` hook provides shared logic for all role mutations with proper cache layer handling for TanStack Query and Prisma Accelerate. Use this hook in canvas-specific wrappers to add markDirty side effects.
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/teams/[teamId]/**/*.{ts,tsx} : Cache-first node system: Role nodes store only `roleId` in node.data. Display data must be fetched from TanStack Query cache using the `useRoleData()` hook which queries the `role.getByTeamId` procedure.
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-12-20T22:12:00.576Z
Learning: Applies to src/app/teams/[teamId]/**/*.tsx : React Flow nodes must store minimal data (e.g., just `roleId`); fetch full Role data from TanStack Query cache in the Node component to keep canvas and sidebars in sync
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/hooks/**/*.ts : For role-metric cache updates, optimistically update both role cache (role.getByTeamId) and dashboard cache (dashboard.getDashboardCharts) to maintain UI consistency
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/server/api/routers/**/*.ts : Invalidate dashboard cache after mutations using invalidateDashboardCache(ctx.db, organizationId, teamId)
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/server/api/routers/**/*.ts : After mutations, invalidate cache tags using `invalidateCacheByTags(ctx.db, [``tag_${id}``])` to trigger background cache updates with Prisma Accelerate.
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/dashboard/[teamId]/**/*.{ts,tsx} : Dashboard cache updates for role-metric assignments must update both `role.getByTeamId` and `dashboard.getDashboardCharts` caches during mutations. Use onMutate for optimistic updates on both caches, then invalidate both on success.

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/server/api/routers/**/*.ts : Invalidate dashboard cache after mutations using invalidateDashboardCache(ctx.db, organizationId, teamId)

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/server/api/routers/**/*.ts : After mutations, invalidate cache tags using `invalidateCacheByTags(ctx.db, [``tag_${id}``])` to trigger background cache updates with Prisma Accelerate.

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/hooks/**/*.ts : For role-metric cache updates, optimistically update both role cache (role.getByTeamId) and dashboard cache (dashboard.getDashboardCharts) to maintain UI consistency

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/**/*role*mutation*.{ts,tsx} : For role mutations, use cache pipeline with two operations: (1) onMutate for optimistic update, (2) onSuccess with both setData(updatedRole) using server response AND invalidate() for background refresh. Always use server response data in setData before invalidate to ensure Prisma Accelerate cache consistency.

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/hooks/**/*.ts : Use optimistic updates with setData before invalidate in TanStack Query mutations to ensure correct data when Prisma Accelerate cache hasn't propagated

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/components/dashboard-metric-card.tsx,src/app/dashboard/[teamId]/_components/public-dashboard-metric-card.tsx : Dashboard metric cards are duplicated with public variant. Consolidate into single component with `readOnly` mode prop instead of maintaining separate components.

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/app/(metric|dashboard)/**/*.tsx : Use three-stage metrics transformation: API → DataPoints (DataIngestionTransformer), DataPoints → ChartConfig (ChartTransformer), ChartConfig → UI (DashboardMetricChart)

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/app/teams/[teamId]/_components/**/*.tsx : Implement cache-first node pattern for role nodes: store only roleId in node data, fetch display data from TanStack Query cache using useRoleData hook

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-20T22:12:00.576Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-12-20T22:12:00.576Z
Learning: Applies to src/lib/metrics/**/*.ts : Metric transformation logic should be organized in `src/lib/metrics/` to support the 3-stage pipeline: Ingestion → Aggregation → Visualization

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T21:27:04.176Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T21:27:04.176Z
Learning: Applies to src/server/api/routers/**/*.ts : Always verify resources belong to user's organization using authorization helpers (getMetricAndVerifyAccess, getRoleAndVerifyAccess, getTeamAndVerifyAccess) in tRPC procedures

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/server/api/routers/**/*.ts : When adding new tRPC routes, use `protectedProcedure` or `workspaceProcedure` and always verify resource belongs to user's organization using authorization helpers (getMetricAndVerifyAccess, getRoleAndVerifyAccess, getTeamAndVerifyAccess) from '@/server/api/utils/authorization'.

Applied to files:

  • src/server/api/routers/metric.ts
📚 Learning: 2025-12-29T12:52:42.935Z
Learnt from: CR
Repo: drifter089/orgOS PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-29T12:52:42.935Z
Learning: Applies to src/app/teams/[teamId]/**/*.{ts,tsx} : Cache-first node system: Role nodes store only `roleId` in node.data. Display data must be fetched from TanStack Query cache using the `useRoleData()` hook which queries the `role.getByTeamId` procedure.

Applied to files:

  • src/server/api/routers/metric.ts
🧬 Code graph analysis (1)
src/server/api/routers/metric.ts (1)
src/server/api/utils/cache-strategy.ts (2)
  • invalidateDashboardCache (197-206)
  • invalidateCacheByTags (167-183)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review

Comment on lines +12 to +15
import {
invalidateCacheByTags,
invalidateDashboardCache,
} from "@/server/api/utils/cache-strategy";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the metric.ts file to see the imports and delete mutation
head -30 src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 1140


🏁 Script executed:

# Check the delete mutation around lines 273-280
sed -n '260,290p' src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 988


🏁 Script executed:

# Look for the metric create input schema to check teamId nullability
rg -A 20 "z\.object.*metric" src/server/api/routers/metric.ts | head -50

Repository: drifter089/orgOS

Length of output: 1229


🏁 Script executed:

# Search for cache tag patterns used in the codebase
rg "invalidateCacheByTags.*team_" src/server/api/ --context 2

Repository: drifter089/orgOS

Length of output: 1960


🏁 Script executed:

# Check the metric model/schema definition for teamId
fd "metric" src/server --type f -name "*.ts" | xargs grep -l "teamId.*z\." | head -5

Repository: drifter089/orgOS

Length of output: 290


🏁 Script executed:

# Get a larger window around the delete mutation to see if there's authz check
sed -n '250,285p' src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 1147


🏁 Script executed:

# Search for the full delete mutation definition
rg -B 10 -A 15 "\.delete.*workspaceProcedure" src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Check the metric schema for teamId optionality
rg -A 30 "export.*metric.*z\.object" src/server/db/schema.ts 2>/dev/null || rg -r "prisma" -A 5 "teamId" src/server/db/schema.prisma 2>/dev/null | head -20

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Look for metric create input schema
rg -B 5 -A 20 "input.*z\.object.*metricId.*z" src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 1396


🏁 Script executed:

# Check if org-level metrics (teamId=null) exist in schema
rg "teamId.*@db\.String" src/server/db/schema.prisma || rg "teamId.*String" src/server/db/schema.prisma

Repository: drifter089/orgOS

Length of output: 195


🏁 Script executed:

# Find Prisma schema file
find . -name "*.prisma" -type f

Repository: drifter089/orgOS

Length of output: 82


🏁 Script executed:

# Check if there's authorization verification for metrics elsewhere
rg "getMetricAndVerifyAccess" src/server/api/routers/metric.ts -B 5 -A 5

Repository: drifter089/orgOS

Length of output: 1019


🏁 Script executed:

# Look for update/create mutations in metric.ts to see if they use authz helpers
rg -B 5 -A 15 "create.*workspaceProcedure\|update.*workspaceProcedure" src/server/api/routers/metric.ts | head -60

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Check the Metric model definition in Prisma
rg -A 20 "^model Metric" prisma/schema.prisma 2>/dev/null || find . -path "*/prisma/*.prisma" -exec rg "model Metric" {} \;

Repository: drifter089/orgOS

Length of output: 3188


🏁 Script executed:

# Verify delete mutation is missing authz by comparing with update mutation
sed -n '230,280p' src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 1384


🏁 Script executed:

# Check how invalidateDashboardCache handles null teamId
rg -A 20 "function invalidateDashboardCache\|export.*invalidateDashboardCache" src/server/api/utils/cache-strategy.ts

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Check how invalidateCacheByTags handles the tags
rg -B 5 -A 25 "function invalidateCacheByTags\|export.*invalidateCacheByTags" src/server/api/utils/cache-strategy.ts

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Verify workspaceProcedure adds organization-level filtering
rg -A 10 "workspaceProcedure" src/server/api/trpc.ts | head -30

Repository: drifter089/orgOS

Length of output: 190


🏁 Script executed:

# Search for cache-strategy file and its functions
cat src/server/api/utils/cache-strategy.ts

Repository: drifter089/orgOS

Length of output: 6332


🏁 Script executed:

# Check if there are any other delete mutations in routers to see the pattern
rg -B 5 -A 20 "\.delete.*workspaceProcedure" src/server/api/routers/ | head -100

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Verify the exact issue: delete doesn't verify org access like update does
rg -B 20 "delete.*workspaceProcedure" src/server/api/routers/metric.ts | grep -E "getMetricAndVerifyAccess|workspaceProcedure|delete:"

Repository: drifter089/orgOS

Length of output: 143


Add authorization check and guard cache tag invalidation in delete mutation.

The delete mutation is missing the getMetricAndVerifyAccess check that the update mutation uses. This allows any authenticated workspace user to delete any metric in the organization, regardless of team access. Additionally, when teamId is null (org-level metrics), the cache tag becomes team_null, wasting rate limits.

Suggested fix
  delete: workspaceProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
-     // Get metric info before deletion for cache invalidation
-     const metric = await ctx.db.metric.findUnique({
-       where: { id: input.id },
-       select: { teamId: true, organizationId: true },
-     });
-
-     if (!metric) {
-       throw new TRPCError({
-         code: "NOT_FOUND",
-         message: "Metric not found",
-       });
-     }
+     // Verify metric belongs to user's organization
+     const metric = await getMetricAndVerifyAccess(
+       ctx.db,
+       input.id,
+       ctx.workspace.organizationId,
+     );

      // Roles with this metric will have metricId set to null (onDelete: SetNull)
      await ctx.db.metric.delete({
        where: { id: input.id },
      });

      // Invalidate Prisma cache for dashboard and role queries
      await invalidateDashboardCache(
        ctx.db,
        metric.organizationId,
        metric.teamId,
      );
-     await invalidateCacheByTags(ctx.db, [`team_${metric.teamId}`]);
+     if (metric.teamId) {
+       await invalidateCacheByTags(ctx.db, [`team_${metric.teamId}`]);
+     }

      return { success: true };
    }),
🤖 Prompt for AI Agents
In @src/server/api/routers/metric.ts around lines 12 - 15, The delete mutation
in src/server/api/routers/metric.ts must perform the same authorization as the
update path: call getMetricAndVerifyAccess(...) to verify the current user/team
can delete the metric before proceeding (mirror the update mutation's check and
error handling). After successful deletion, change the cache invalidation so it
doesn't emit a tag like team_null: only include the team tag when metric.teamId
is non-null (e.g., push/invalidate `team_${teamId}` only if teamId exists) and
always include org-level tags as done elsewhere; update the calls to
invalidateCacheByTags and invalidateDashboardCache accordingly so org-level
metrics do not generate a team_null tag.

@drifter089 drifter089 merged commit a907369 into main Jan 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants