# 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).
