# Split queries.ts: Errors + Query-Key Factory + Domain Modules — Design **Date:** 2026-06-08 **Status:** Approved (brainstorming) — ready for implementation planning. **Issue:** #65 (split the 584-line `queries.ts`, extract the error classes, add a query-key factory, decide the search-invalidation gap). ## Context `web/src/api/queries.ts` is 584 lines spanning 8 domains (error classes, auth, objects, fields-on-object, field-definitions, vocab, terms, authorities, search). No hook crosses a domain boundary, so a split is low-risk. Three concrete problems: - **Toast path drags in the hook module.** After #63, `query-client.ts` imports `errorMessageKey` from `error-message.ts`, which imports `{ HttpError, InUseError }` from `./queries` — so the toast wiring transitively loads all the hooks. Extracting the error classes breaks that chain. - **No query-key factory.** ~20 key literals (`["objects", params]`, `["object", id]`, `["terms", vocabularyId]`, …) plus `config-provider.tsx`'s `["config"]` are hand-written and typo-prone. - **Object writes don't invalidate `["search"]`.** `useUpdateObject`/`useDeleteObject`/`useSetVisibility` invalidate `["objects"]`/`["object", id]` but never the search index, so the search panel keeps stale hits until the user re-searches. **Decision (brainstorming): invalidate** — conventional eventually-consistent behaviour; the query is `enabled` only with a term, so it's a no-op when not searching. **Constraint:** ~30 files import from `../api/queries`. The public import path must stay stable (a barrel), so the refactor touches the data layer only — feature files don't change. ## Components ### `api/errors.ts` (new) — canonical home for the 4 error classes Move `HttpError`, `FieldRejection`, `InUseError`, `VisibilityError` here verbatim (no behaviour change). `error-message.ts` changes its import from `./queries` to `./errors` — this is the decoupling: the toast path (`query-client → error-message → errors`) no longer loads the hook module. The barrel (below) **re-exports** the error classes (`export * from "../errors"`), so the existing importers that pull them from `../api/queries` — `object-edit-form.tsx`, `object-new-page.tsx`, `publish-control.tsx`, `search-panel.tsx`, and the tests `mutation-error.test.tsx` / `labelled-record-row.test.tsx` / `error-message.test.ts` — keep working unchanged. ### `api/query-keys.ts` (new) — one key factory ```ts export type ObjectListParams = { limit: number; offset: number; sort?: string; order?: "asc" | "desc"; visibility?: string; q?: string; }; export const keys = { me: () => ["me"] as const, config: () => ["config"] as const, objects: () => ["objects"] as const, // family prefix (invalidation) objectsPage: (p: ObjectListParams) => ["objects", p] as const, object: (id: string) => ["object", id] as const, fieldDefinitions: () => ["field-definitions"] as const, vocabularies: () => ["vocabularies"] as const, terms: (vocabularyId: string) => ["terms", vocabularyId] as const, authorities: (kind: string) => ["authorities", kind] as const, search: () => ["search"] as const, // family prefix (invalidation) searchResults: (term: string, visibility: string | null) => ["search", term, visibility] as const, }; ``` - Every `queryKey:` / `invalidateQueries` / `setQueryData` site in the query modules **and** `config-provider.tsx`'s `["config"]` routes through `keys.*` — producing the identical arrays, so cache behaviour is unchanged. (`keys.objects()` prefix-matches `keys.objectsPage(p)` exactly as `["objects"]` matches `["objects", params]` today.) - `ObjectListParams` lives here (it *is* part of the key); `objects.ts` imports it from here — one direction, no import cycle. (It moves out of `queries.ts`'s objects section.) ### `api/queries/` (new directory) — split by domain + barrel ``` api/ client.ts (unchanged) errors.ts (new) query-keys.ts (new) query-client.ts (unchanged — already imports only error-message) error-message.ts (one-line change: import errors from ./errors) queries/ index.ts barrel: `export * from "./auth"; … ; export * from "../errors";` auth.ts useMe, useLogin, useLogout objects.ts useObjectsPage, useObject, useCreateObject, useUpdateObject, useSetFields, useDeleteObject, useSetVisibility field-defs.ts useFieldDefinitions, useCreateFieldDefinition, useUpdateFieldDefinition, useDeleteFieldDefinition vocab.ts useVocabularies, useCreateVocabulary, useRenameVocabulary, useDeleteVocabulary, useTerms, useAddTerm, useUpdateTerm, useDeleteTerm authorities.ts useAuthorities, useCreateAuthority, useUpdateAuthority, useDeleteAuthority search.ts useSearch, SEARCH_PAGE ``` `api/queries.ts` is deleted; `import { … } from "../api/queries"` resolves to `api/queries/index.ts`. Each module imports `api` from `../client`, types from `../schema`, error classes from `../errors`, and `keys` from `../query-keys`. The hook bodies are moved verbatim (only their key literals → `keys.*`). ### `api/query-client.ts` No change needed — after #63 it imports only `errorMessageKey`. (Once `error-message.ts` points at `./errors`, the `query-client → error-message → queries` hook dependency is gone.) ### Search invalidation (`objects.ts`) `useUpdateObject`, `useDeleteObject`, `useSetVisibility` add `void qc.invalidateQueries({ queryKey: keys.search() })` alongside their existing object invalidations. ## Data flow / behaviour Identical to today except: after an object update/delete/visibility-change, an active search query refetches (eventually-consistent with the Meilisearch reindex). All keys, hooks, error classes, and endpoints are otherwise unchanged. ## Error handling / edges - The barrel re-exporting errors means the classes are importable from both `../api/errors` (canonical) and `../api/queries` (compat) — intentional, to avoid churning ~6 importers + tests. - `as const` keys are readonly tuples; TanStack Query accepts readonly arrays as keys — no type issue. - No import cycle: `query-keys.ts` exports `ObjectListParams` + `keys` and imports nothing from `queries/`; `objects.ts` imports both from `query-keys.ts`. - The search query stays `enabled: term.length > 0`, so invalidating `["search"]` is a no-op when no search is active. ## Testing - **Behavior guard (must pass unchanged):** `queries.test.ts`, `queries.authoring.test.tsx`, `queries.fields.test.tsx`, `queries.search.test.tsx`, `queries.visibility.test.tsx`, `queries.vocab.test.tsx`, `mutation-feedback.test.tsx`, `error-message.test.ts`, `mutation-error.test.tsx`, `labelled-record-row.test.tsx` — all import from `../api/queries` (the barrel) and from the re-exported error classes; they must not need edits. The full app suite is the integration guard. - **New `query-keys.test.ts`:** assert the factory arrays — e.g. `keys.objects()` → `["objects"]`, `keys.objectsPage(p)` → `["objects", p]`, `keys.object("x")` → `["object", "x"]`, `keys.searchResults("q", null)` → `["search", "q", null]`. - **Search-invalidation assertion:** extend `queries.visibility.test.tsx` (or `.authoring`) to seed an active `["search", …]` query in the cache, run an object update/delete/set-visibility, and assert the search query is invalidated (e.g. its `isInvalidated`/refetch fires). - **Gate:** `typecheck`/`lint`/`test`/`build`/`check:size`/`check:colors` green; no new dependency; no new i18n keys; no codename; `check:size` unchanged (pure reorg + one invalidate call). ## Acceptance criteria 1. `api/errors.ts` holds the 4 error classes; `error-message.ts` imports them from `./errors`; the barrel re-exports them so every existing importer (components + tests) still resolves. 2. `queries.ts` is split into `api/queries/{auth,objects,field-defs,vocab,authorities,search}.ts` + `index.ts`; `api/queries.ts` is deleted; the `../api/queries` import path is unchanged for all ~30 consumers. 3. A `keys` factory in `api/query-keys.ts` is used by every `queryKey`/invalidate/setQueryData site, including `config-provider.tsx`. 4. `useUpdateObject`/`useDeleteObject`/`useSetVisibility` invalidate `keys.search()`. 5. All existing tests pass unchanged; `typecheck`/`lint`/`build`/`check:colors` green; `check:size` unchanged; no new dependency; no new i18n keys; no codename. ## Out of scope → follow-ups - No hook signature/behaviour changes beyond the search invalidation; no endpoint changes. - `staleTime` / `keepPreviousData` choices stay as-is (intentional per #63's note). - Repointing the 4 component error-importers from the barrel to `../api/errors` (the re-export keeps them working; a later cosmetic cleanup could do it).