mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
refactor(query-keys): route remaining bare arrays through the factory
The target editor invalidated a bare ["insights"] array on save and reset. That literal sat outside the centralised factory, so the broad insights fan-out it intended never matched the reads, which key through insightsTargets/insightsRoot. Route the editor's reads and invalidations through userThresholds(), insightsRoot(), insightsTargets() and analytics() so a saved target actually refreshes every insights surface. Migrate the other call sites the lint rule does not reach onto the factory: the Coach conversation rail + thread reads, the per-metric personal-records badge, the per-day measurement drill-down, the dashboard intake compliance-chart fan-out, the per-medication side-effects list, the onboarding auth invalidations, the notification-preferences read, and the PWA persister's auth-me lookup. Add the matching factory keys: coachConversations, coachConversation, personalRecordsByMetric, measurementDrilldown, complianceChartInline and medicationSideEffects. Each new key reuses the prefix its reads already use so no invalidation shape drifts.
This commit is contained in:
1 parent
3e04035de8
commit
bda06c9ec4
16 files changed
+80
-24
No files matched your search
@@ -281,7 +281,7 @@ export function MedicationIntakeQuickAdd({
|
||||
// Fan-out to every inline compliance chart key (one per medication)
|
||||
// so the detail-page tile refreshes when the user re-enters it.
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["compliance-chart-inline"],
|
||||
queryKey: queryKeys.complianceChartInline(),
|
||||
});
|
||||
|
||||
toast.success(t("common.saved"));
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
CoachSuggestion,
|
||||
} from "@/lib/ai/coach/types";
|
||||
import { apiDelete, apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B2b — TanStack Query + SSE client for the AI Coach
|
||||
@@ -32,8 +33,8 @@ import { apiDelete, apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
|
||||
*/
|
||||
|
||||
const QUERY_KEYS = {
|
||||
list: () => ["coachConversations"] as const,
|
||||
one: (id: string) => ["coachConversation", id] as const,
|
||||
list: () => queryKeys.coachConversations(),
|
||||
one: (id: string) => queryKeys.coachConversation(id),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,7 +75,7 @@ export function useCoachConversation(id: string | null) {
|
||||
// never fires when `id` is null. Key directly on `id` (TanStack
|
||||
// Query accepts `null` in the array) — the previous "null"
|
||||
// placeholder branch was structurally equivalent dead code.
|
||||
queryKey: ["coachConversation", id] as const,
|
||||
queryKey: queryKeys.coachConversation(id),
|
||||
queryFn: async (): Promise<CoachConversationDetailDTO> => {
|
||||
if (!id) throw new Error("missing id");
|
||||
return apiGet<CoachConversationDetailDTO>(`/api/insights/chat/${id}`, {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -75,7 +76,7 @@ export function PersonalRecordBadge({
|
||||
// Per-metric queryKey so two tiles for two different metrics never
|
||||
// share a cache slot (HealthLog convention — see
|
||||
// `feedback_react_query_key_collision`).
|
||||
queryKey: ["personal-records", "by-metric", metricType],
|
||||
queryKey: queryKeys.personalRecordsByMetric(metricType),
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const data = await apiGet<PersonalRecordRow[] | null>(
|
||||
|
||||
@@ -1508,7 +1508,7 @@ function DayDrillDown({
|
||||
const { t, locale } = useTranslations();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["measurement-drilldown", type, dayKey],
|
||||
queryKey: queryKeys.measurementDrilldown(type, dayKey),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("type", type);
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "@/lib/medications/side-effects/taxonomy";
|
||||
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
|
||||
import { apiDelete, apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* v1.4.25 W19d — GLP-1 side-effect section for the medication detail
|
||||
@@ -114,12 +115,7 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
|
||||
const [footerEl, setFooterEl] = useState<HTMLDivElement | null>(null);
|
||||
const formId = useId();
|
||||
|
||||
const listKey = [
|
||||
"medications",
|
||||
medicationId,
|
||||
"side-effects",
|
||||
"list",
|
||||
] as const;
|
||||
const listKey = queryKeys.medicationSideEffects(medicationId);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: listKey,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { apiGet, apiPost, apiPut } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
AnamnesisCard,
|
||||
buildAnamnesisAboutMeBody,
|
||||
@@ -141,7 +142,7 @@ export function BaselineForm() {
|
||||
}
|
||||
}
|
||||
await apiPost("/api/onboarding/step", { step: 4 });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/onboarding/4");
|
||||
} catch (err) {
|
||||
const message =
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* v1.4.25 W14b-Content — onboarding step 1 (goals).
|
||||
@@ -127,7 +128,7 @@ export function GoalsChipPicker({ userId: _userId }: GoalsChipPickerProps) {
|
||||
step: 2,
|
||||
goals: Array.from(selected),
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/onboarding/2");
|
||||
} catch (err) {
|
||||
const message =
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* Onboarding step 2 (source).
|
||||
@@ -105,7 +106,7 @@ export function SourceCardGrid() {
|
||||
setAdvancing(true);
|
||||
try {
|
||||
await apiPost("/api/onboarding/step", { step: 3 });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/onboarding/3");
|
||||
} catch (err) {
|
||||
const message =
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { scrollBehaviorForUser } from "@/lib/motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { DISCLAIMER_VERSION } from "@/lib/onboarding/disclaimer";
|
||||
|
||||
/**
|
||||
@@ -152,7 +153,7 @@ export function WelcomeCarousel() {
|
||||
});
|
||||
}
|
||||
await apiPost("/api/onboarding/step", { step: 1 });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/onboarding/1");
|
||||
} catch (err) {
|
||||
const message =
|
||||
|
||||
@@ -239,7 +239,7 @@ export function GettingStartedChecklist() {
|
||||
});
|
||||
|
||||
const { data: notificationsData } = useQuery<NotificationsPreferences>({
|
||||
queryKey: ["notifications", "preferences"],
|
||||
queryKey: queryKeys.notificationsPreferences(),
|
||||
queryFn: async () => {
|
||||
return apiGet("/api/notifications/preferences");
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { OnboardingTour } from "./tour";
|
||||
|
||||
const POST_WIZARD_GRACE_MS = 1500;
|
||||
@@ -371,7 +372,7 @@ export function TourLauncher({ ready = true }: TourLauncherProps) {
|
||||
if (user?.id) writeSessionDismissed(user.id);
|
||||
try {
|
||||
await apiPost("/api/onboarding/tour", { completed: true, outcome });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
} catch {
|
||||
// Swallow — the session-dismiss flag prevents a re-open.
|
||||
// Next visit will retry via the same code path.
|
||||
|
||||
@@ -144,7 +144,7 @@ function TargetEditSheetBody({
|
||||
// tone vs. the persisted override). The body only mounts when the
|
||||
// dialog is open, so we don't need a separate `enabled` flag.
|
||||
const { data: thresholdsData } = useQuery({
|
||||
queryKey: ["user", "thresholds"],
|
||||
queryKey: queryKeys.userThresholds(),
|
||||
queryFn: async () => {
|
||||
return apiGet<ThresholdsApiResponse>("/api/user/thresholds");
|
||||
},
|
||||
@@ -227,10 +227,10 @@ function TargetEditSheetBody({
|
||||
await apiPut("/api/user/thresholds", payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "thresholds"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userThresholds() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.analytics() });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
toast.success(t("targets.edit.saveSuccess"));
|
||||
onOpenChange(false);
|
||||
},
|
||||
@@ -253,10 +253,10 @@ function TargetEditSheetBody({
|
||||
await apiDelete(`/api/user/thresholds?metric=${metric}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "thresholds"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userThresholds() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.analytics() });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
toast.success(t("targets.edit.resetSuccess"));
|
||||
onOpenChange(false);
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user