refactor(query-keys): route long-tail surfaces through the factory

The settings / medications / admin / hooks tree still declared
bare-literal `queryKey: [ … ]` arrays at ~70 call sites. Centralise
them through `queryKeys.<entry>()` so mutation-invalidation prefixes
stay symmetrical and a future rename only touches one location.

New factory entries cover per-medication compliance / titration /
cadence / glp1-details / drug-level-chart / intake-list, the
Withings status read, the paginated admin audit-log filter, and the
workouts-recent list opts. Existing entries cover everything else
(passkeys, tokens, settings, telegram, integrations, research-mode,
admin status / users / backups / feedback / host-metrics / app-logs /
ai-quality / assistant-flags, coach-prefs, feature-flags, etc.).

Lockstep updates:

- `eslint-plugins/healthlog/queryKey-factory.js` extends
  `GUARDED_DIRECTORIES` to cover `src/components/{settings,
  medications,admin}` + `src/hooks`, plus the three guarded files
  under `src/app/medications/**` and `src/app/targets/page.tsx`.
- `src/lib/__tests__/query-keys.test.ts` mirrors the guard's
  `guardedRoots` and pins the shape of every new factory entry.

Tests stay green: 4 736 unit; the factory walker reports zero
bare-literal `queryKey:` in any newly-guarded surface.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 19:04:22 +02:00
1 parent 6850818271
commit e2018b2d15
43 files changed
+332 -118

No files matched your search

+78 -2
View File
@@ -100,6 +100,74 @@ describe("queryKeys factory", () => {
"dashboard-medication-compliance",
);
});
// v1.4.42 W3-QUERYKEY-LONGTAIL — pin the shapes of the new factory
// entries so a future rename can't drift the cache layout silently.
it("exposes the per-medication sub-keys under the medications prefix", () => {
expect(queryKeys.medicationCompliance("med1")).toEqual([
"medications",
"med1",
"compliance",
]);
expect(queryKeys.medicationTitration("med1")).toEqual([
"medications",
"med1",
"titration",
]);
expect(queryKeys.medicationCadence("med1")).toEqual([
"medications",
"med1",
"cadence",
]);
expect(queryKeys.medicationGlp1Details("med1")).toEqual([
"medications",
"med1",
"glp1-details",
]);
expect(queryKeys.medicationIntakeDrugLevelChart("med1")).toEqual([
"medications",
"med1",
"intake",
"drug-level-chart",
]);
const listKey = queryKeys.medicationIntakeList("med1", {
sortBy: "takenAt",
sortDir: "desc",
limit: 25,
offset: 0,
status: "completed",
});
expect(listKey[0]).toBe("medications");
expect(listKey[1]).toBe("med1");
expect(listKey[2]).toBe("intake");
expect(listKey[3]).toBe("list");
});
it("exposes withingsStatus under the withings prefix", () => {
expect(queryKeys.withingsStatus()).toEqual(["withings", "status"]);
});
it("packs adminAuditLogFiltered params into a stable filtered shape", () => {
const key = queryKeys.adminAuditLogFiltered({
filter: "all",
page: 1,
perPage: 50,
actor: "",
actionFilter: "",
target: "",
range: "7d",
});
expect(key[0]).toBe("admin");
expect(key[1]).toBe("audit-log");
expect(key[2]).toBe("filtered");
expect(key.length).toBe(10);
});
it("packs workoutsRecentList opts under the workouts-recent prefix", () => {
const key = queryKeys.workoutsRecentList({ limit: 3 });
expect(key[0]).toBe("workouts");
expect(key[1]).toBe("recent");
});
});
describe("dependent-key bundles", () => {
@@ -195,11 +263,19 @@ describe("queryKey factory enforcement", () => {
join(repoRoot, "src", "app", "page.tsx"),
join(repoRoot, "src", "hooks", "use-auth.ts"),
// v1.4.41 W-FRONTEND-FACTORY — auth, notifications, and the
// about-section migrated to the factory. Future waves extend
// this list as they migrate their own surface.
// about-section migrated to the factory.
join(repoRoot, "src", "app", "auth"),
join(repoRoot, "src", "app", "notifications"),
join(repoRoot, "src", "components", "settings", "about-section.tsx"),
// v1.4.42 W3-QUERYKEY-LONGTAIL — settings / medications /
// admin / hooks now route every read through the factory.
join(repoRoot, "src", "components", "settings"),
join(repoRoot, "src", "components", "medications"),
join(repoRoot, "src", "components", "admin"),
join(repoRoot, "src", "hooks"),
join(repoRoot, "src", "app", "medications", "page.tsx"),
join(repoRoot, "src", "app", "medications", "[id]", "history", "page.tsx"),
join(repoRoot, "src", "app", "targets", "page.tsx"),
];
function collect(): string[] {
+77
View File
@@ -73,6 +73,38 @@ export const queryKeys = {
medicationDetail: (id: string) => ["medications", id] as const,
medicationComplianceChart: (medicationId: string) =>
["compliance-chart-inline", medicationId] as const,
/**
* v1.4.42 W3-QUERYKEY-LONGTAIL — per-medication compliance KPI used
* by `medication-card` + `glp1-medication-card`. Centralising lets
* the intake mutation invalidate every compliance read through the
* `["medications"]` prefix instead of one bare-literal at a time.
*/
medicationCompliance: (medicationId: string) =>
["medications", medicationId, "compliance"] as const,
medicationTitration: (medicationId: string) =>
["medications", medicationId, "titration"] as const,
medicationCadence: (medicationId: string) =>
["medications", medicationId, "cadence"] as const,
medicationGlp1Details: (medicationId: string) =>
["medications", medicationId, "glp1-details"] as const,
medicationIntakeDrugLevelChart: (medicationId: string) =>
["medications", medicationId, "intake", "drug-level-chart"] as const,
/**
* v1.4.42 — intake-history list with sort / paging / status filter.
* The opaque params object lives at index 4 so the
* `["medications", id, "intake", "list"]` prefix invalidates every
* sort/page combination on an intake mutation.
*/
medicationIntakeList: (
medicationId: string,
params: {
sortBy: string;
sortDir: string;
limit: number;
offset: number;
status: string;
},
) => ["medications", medicationId, "intake", "list", params] as const,
/**
* v1.4.40 W-RSC — the dashboard-level compliance chart (aggregate
* across every scheduled medication) was a bare `["medication-
@@ -143,11 +175,44 @@ export const queryKeys = {
["admin", "host-metrics", window] as const,
adminAuditActions: () => ["admin", "audit-log", "actions"] as const,
adminAuditOverview: () => ["admin", "audit-log", "overview-preview"] as const,
/**
* v1.4.42 W3-QUERYKEY-LONGTAIL — paginated + filtered audit-log
* read used by the login-overview admin section. The `filtered`
* discriminator at index 2 keeps the no-arg `adminAuditLog(filter)`
* cache slot byte-distinct so its consumers don't collide.
*/
adminAuditLogFiltered: (params: {
filter: string;
page: number;
perPage: number;
actor: string;
actionFilter: string;
target: string;
range: string;
}) =>
[
"admin",
"audit-log",
"filtered",
params.filter,
params.page,
params.perPage,
params.actor,
params.actionFilter,
params.target,
params.range,
] as const,
tokens: () => ["tokens"] as const,
telegram: () => ["telegram"] as const,
telegramSettings: () => ["telegram", "settings"] as const,
withings: () => ["withings"] as const,
/**
* v1.4.42 W3-QUERYKEY-LONGTAIL — the per-card Withings status read.
* Shares the `["withings"]` prefix with `withings()` so a disconnect
* mutation invalidates both at once.
*/
withingsStatus: () => ["withings", "status"] as const,
// v1.4.32 — workout list + detail caches. `workouts()` is the
// root key invalidated by the batch-ingest mutation; the recent +
@@ -155,6 +220,18 @@ export const queryKeys = {
// detail page share a cache slot with the list page.
workouts: () => ["workouts"] as const,
workoutsRecent: () => ["workouts", "recent"] as const,
/**
* v1.4.42 W3-QUERYKEY-LONGTAIL — the `useWorkouts` hook used to
* spread `workoutsRecent()` and append an opts object inline. The
* factory now owns the full shape so the hook never reaches for a
* literal-array wrapper.
*/
workoutsRecentList: (opts: {
limit?: number;
offset?: number;
since?: string;
sportType?: string;
}) => ["workouts", "recent", opts] as const,
workoutDetail: (id: string) => ["workouts", id] as const,
adminSettings: () => ["admin", "settings"] as const,