docs(specs): split queries.ts — errors + key factory + domain modules (#65)

This commit is contained in:
2026-06-08 20:37:11 +02:00
parent 404cf67f35
commit 7ddf6967ce
@@ -0,0 +1,146 @@
# 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).