Implementation plan 1 for Data normalizationhow can we use Tanstack query to create a better data normalization strategy for our system.

    Approach: Hop-Preserving Cache with Select-Time Resolution

      This is what will happen:

        Store each hop in cache as returned by API (raw response with aliasAccount/redirectInfo fields)

        Use select function to traverse the chain at query time

        Eagerly fetch targets when encountering aliases/redirects

        1

        Use metadata to track reverse relationships for invalidation

        Hybrid updates: invalidate queries (triggers select re-run) + optional optimistic updates

      Phase 1: Account Aliases (Start Here)

        Step 1: Modify fetchAccount to populate intermediate hops

          Location: frontend/packages/shared/src/models/entity.ts:159-181

          Changes:

          1
          async function fetchAccount(
            id: string, 
            queryClient: QueryClient // Add this parameter
          ): Promise<HMAccount> {
            const account = await client.accounts.getAccount(id)
            
            // NEW: Store this hop in cache immediately
            queryClient.setQueryData(['account', id], account)
            
            if (account.aliasAccount) {
              // Recursively fetch target
              return fetchAccount(account.aliasAccount, queryClient)
            }
            
            return account
          }
          

        Why: This ensures every hop is stored in cache as we traverse the chain.

        Step 2: Create alias chain traversal helper

          New file or add to: frontend/packages/shared/src/models/entity.ts

          Add function:

          function followAccountAliasChain(
            startId: string,
            queryClient: QueryClient,
            maxDepth = 5 // Prevent infinite loops
          ): HMAccount | undefined {
            const visited = new Set<string>()
            let currentId = startId
            let depth = 0
            
            while (depth < maxDepth && !visited.has(currentId)) {
              visited.add(currentId)
              
              const account = queryClient.getQueryData<HMAccount>(['account', currentId])
              
              if (!account) {
                return undefined // Not in cache
              }
              
              if (!account.aliasAccount) {
                return account // Found final target
              }
              
              currentId = account.aliasAccount
              depth++
            }
            
            return undefined // Cycle detected or max depth reached
          }
          
          1

          Why: Reusable logic for traversing alias chains by reading from cache.

        Step 3: Update useAccount hook with select function

          Location: frontend/packages/shared/src/models/entity.ts:186-202

          Changes:

          export function useAccount(id: string | undefined, options?: UseAccountOptions) {
            const { client, queryClient } = useUniversalClient()
            
            return useQuery({
              queryKey: [queryKeys.ACCOUNT, id],
              queryFn: () => fetchAccount(id!, queryClient), // Pass queryClient
              enabled: !!id && (options?.enabled ?? true),
              
              // NEW: Add select to traverse chain
              select: (rawAccount) => {
                if (!rawAccount.aliasAccount) {
                  return rawAccount // No alias, return as-is
                }
                
                // Follow the chain
                const resolved = followAccountAliasChain(id!, queryClient)
                return resolved || rawAccount // Fallback to raw if chain incomplete
              },
              
              ...options,
            })
          }
          

          Why: The hook now returns the final resolved account data, but cache preserves all hops.

        Step 4: Update batch resolver to track relationships

        1

          Location: frontend/packages/shared/src/models/entity.ts:80-161

          Changes: Modify createBatchAccountsResolver to use setQueryData with metadata:

          // After resolving aliases, store with metadata
          Object.entries(resolvedMap).forEach(([sourceId, targetAccount]) => {
            const targetId = targetAccount.id
            
            // Store source (if it was an alias)
            if (sourceId !== targetId) {
              queryClient.setQueryData(
                ['account', sourceId],
                { ...originalAccount, aliasAccount: targetId },
                { meta: { isAlias: true, targetId } }
              )
            }
            
            // Store target with reverse tracking
            const existingMeta = queryClient.getQueryState(['account', targetId])?.meta
            const aliasedFrom = (existingMeta?.aliasedFrom || []) as string[]
            
            queryClient.setQueryData(
              ['account', targetId],
              targetAccount,
              { meta: { aliasedFrom: [...new Set([...aliasedFrom, sourceId])] } }
            )
          })

          Why: Tracks which accounts point to each target for smart invalidation.

        Step 5: Add smart invalidation to account mutations

          Locations: Any mutations that update accounts (e.g., updateProfile, profile updates)

          Pattern to add:

          onSuccess: (updatedAccount, variables) => {
            const accountId = variables.account || 

          Why: When target changes, all sources automatically see the update through select re-execution.

        Step 6: Handle orphaned references

          In fetchAccount: Add eager fetching of missing targets:

          1
          if (account.aliasAccount) {
            // Check if target is in cache
            const targetInCache = queryClient.getQueryData(['account', account.aliasAccount])
            
            if (!targetInCache) {
              // Eagerly fetch target
              queryClient.fetchQuery({
                queryKey: ['account', account.aliasAccount],
                queryFn: () => fetchAccount(account.aliasAccount!, queryClient)
              })
            }
            
            return fetchAccount(account.aliasAccount, queryClient)
          }

          Why: Ensures complete chain is available in cache for select function.

      Phase 2: Extend to Documents (Later)

        Step 7: Apply pattern to document redirects

          Locations:

            frontend/packages/shared/src/resource-loader.ts

            frontend/packages/shared/src/models/entity.ts (useResource hook)

          Similar changes:

            Store each hop when fetching resources

            Create followRedirectChain helper (similar to account version)

            Add select function to useResource that traverses redirects

            Update mutations to invalidate redirect chains

            Handle redirectInfo field in DocumentInfo responses

    Testing Strategy

      Unit tests for followAccountAliasChain:

        Single hop (A → B)

        Multi-hop (A → B → C)

        Cycle detection (A → B → A)

        Max depth limit

        Missing intermediate hops

      Integration tests for useAccount:

      1

        Fetch aliased account, verify final data returned

        Verify all hops stored in cache

        Update target, verify source queries update

        Test with concurrent queries for same chain

      Manual testing:

        Profile page showing aliased account

        Update target account profile, verify UI updates

        Check React DevTools Query tab for cache structure

    Summary

      What this achieves:

        ✅ Preserves all hops in cache (A → B → C all stored separately)

        ✅ Components get resolved data transparently

        ✅ Updates propagate automatically via invalidation + select re-run

        ✅ Handles orphaned references by eager fetching

        ✅ Works with existing batch resolver pattern

        ✅ Acceptable performance for edge cases (50+ aliases)

        ✅ No persistent storage needed (rebuilds on next fetch)

      What's NOT in scope (by design):

        ❌ Direct cache updates on mutations (relies on invalidation)

        ❌ Persistent relationship storage across sessions

        ❌ Complex garbage collection prevention