Files
textbee/web/lib/api/query-keys.ts
isra el c1942a4ba3 fix: repair two cache invalidations that silently matched nothing
Both bugs were hand-written query keys. lib/api/query-keys.ts exists to
prevent exactly this and neither site used it.

1. /auth/who-am-i was cached under two keys. Four call sites used
   ['whoAmI'] and seven used ['currentUser'], including the typed
   useCurrentUser hook. So the endpoint was fetched twice on any page
   using both, and invalidating one never invalidated the other:
   edit-profile-form refreshed ['currentUser'] and left the email
   verification banner reading stale data under ['whoAmI'], while
   use-onboarding had the mirror-image bug.

   verify-email/page.tsx additionally cached the whole axios response
   where the others cached the unwrapped user, so unifying the key meant
   normalising that shape too. One key demands one shape.

2. generate-api-key invalidated ['apiKeys', 'stats'], which no query
   uses. react-query matches by prefix, so it matched neither the key
   list (['apiKeys', status]) nor the dashboard stats (['stats']).
   Generating an API key refreshed nothing at all. It is now three
   invalidations against queryKeys, using a new apiKeysAll prefix so
   every status filter refreshes rather than just 'active'.

Also added accountDeletionRequestedAt to the User type. It is a real
field on the user schema and the deletion banner already reads it; only
the type did not know about it.

Both bugs are invisible on inspection, which is how they shipped, so the
regression tests were each confirmed to fail against the pre-change code
before being trusted.

Verified: build clean, 0 lint errors (21 warnings, unchanged), 155 unit
tests (from 153), 78 e2e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 04:02:30 +03:00

26 lines
1.2 KiB
TypeScript

import type { ApiKeyStatusFilter } from './types'
// Single source of truth for react-query cache keys. Values intentionally match
// the ad-hoc string keys used across the app before this refactor (e.g.
// ['devices'], ['currentSubscription'], ['apiKeys', 'active']) so migrated and
// not-yet-migrated components still share the same cache entries.
export const queryKeys = {
currentUser: ['currentUser'] as const,
subscription: ['currentSubscription'] as const,
stats: ['stats'] as const,
devices: ['devices'] as const,
webhooks: ['webhooks'] as const,
billingPlans: ['billingPlans'] as const,
apiKeys: (status: ApiKeyStatusFilter = 'active') =>
['apiKeys', status] as const,
// Prefix covering every apiKeys list regardless of status filter. Use this
// to invalidate: a new or revoked key changes the active, revoked and all
// lists at once, and invalidating only apiKeys('active') leaves the other
// two serving stale data.
apiKeysAll: ['apiKeys'] as const,
deviceMessages: (deviceId: string, filters?: Record<string, unknown>) =>
filters
? (['messages', deviceId, filters] as const)
: (['messages', deviceId] as const),
}