@oc-mui/query
v2.0.0
Published
Data-fetching and plugin-config facade (TanStack Query) for the Opencast Management UI and its plugins.
Readme
@oc-mui/query
Data-fetching layer. Wraps TanStack Query and graphql-request behind a single workspace import, exposes typed GraphQL hooks generated from .graphql files, and owns the definePluginConfig reader API.
Contract: implements the Config Contract — see docs/reference/contracts.md and docs/reference/configuration.md. Public API surface tracked in etc/query.api.md.
The wrapper rule
This package is the only place in the workspace allowed to import from @tanstack/react-query. Apps, plugins, and other packages must import useQuery, useMutation, QueryClient, … from here. The rule is enforced by no-restricted-imports in @oc-mui/eslint-config with an explicit exception for this directory.
If we ever need to swap or major-bump the underlying library, we change it here without touching plugins or apps.
Usage
Read your plugin's config slice
import { z } from "zod";
import { definePluginConfig } from "@oc-mui/query";
const schema = z.object({
pageSize: z.number().int().positive().default(20),
});
export const myPluginConfig = definePluginConfig({
id: "my-plugin",
schema,
defaults: { pageSize: 20 },
});
// In a component:
const { pageSize } = myPluginConfig.use();
// In an event handler:
const { pageSize } = myPluginConfig.read();Run a typed GraphQL query
import { useMuiGetMyEventsQuery } from "@oc-mui/query";
const { data, isLoading } = useMuiGetMyEventsQuery({ limit: 20 });
const events = (data?.currentUser?.myEvents?.nodes ?? [])
.filter((e): e is NonNullable<typeof e> => e !== null);Surface
| Symbol | Purpose |
|--------|---------|
| QueryProvider | Wraps the app in a TanStack QueryClientProvider. The shell mounts it; plugins consume the client through hooks. |
| definePluginConfig({ id, schema, defaults }) | Builds a typed config reader. Returns { id, schema, defaults, register, use, read }. Reading another plugin's slice is forbidden — enforced by the local/no-cross-plugin-config lint rule. |
| useAppConfig() | Reads the merged AppConfig. Source of truth for app.theme, app.locale, app.enabledPlugins. |
| useGetUserInfo() | Currently-signed-in user. |
| Generated hooks (useMuiGetMyEventsQuery, …) | One hook per .graphql operation (operation names carry the mandatory Mui prefix), regenerated by GraphQL Codegen into src/gql-generated.ts. |
| EVENT_SORTABLE_FIELDS, SERIES_SORTABLE_FIELDS, PLAYLIST_SORTABLE_FIELDS | Runtime lists of the fields the backend accepts in EventOrderByInput / SeriesOrderByInput / PlaylistOrderByInput. Derived from the schema by codegen (see below); fed into restrictSortingToFields from @oc-mui/ui so tables only offer sorts the backend supports. |
| gql (re-export from graphql-request) | For ad-hoc operations; prefer codegen for anything reused. |
For the exhaustive list, see etc/query.api.md.
Code generation
Two source files in this package are generated from the committed schema — src/schema.graphql — and committed to the repo, so the rest of the workspace (and CI) builds and regenerates without a backend:
| Generated file | Produced by | Contains |
|----------------|-------------|----------|
| src/gql-generated.ts | @graphql-codegen typescript + typescript-operations + typescript-react-query plugins (plus add for the two header lines) | Types and React Query hooks for every .graphql operation. The Mui…Document / Mui…FragmentDoc constants are plain strings. |
| src/schema-input-fields.generated.ts | the local codegen-plugins/input-field-names.mjs plugin | Runtime as const arrays of the field names in each *OrderByInput / *FilterByInput type. TypeScript input types are erased at runtime, so this is how the UI knows which fields are orderable/filterable without hardcoding them. |
You only need to run codegen when an operation or the schema changes — not as part of normal setup or per-build. The generated files are committed; a fresh clone builds fine without ever running codegen. pnpm codegen:check (part of pnpm verify and CI) regenerates from the committed schema and fails on any diff, so inputs and outputs cannot drift apart.
Adding or changing an operation (.graphql files) needs no backend at all:
pnpm --filter @oc-mui/query codegenWhen the backend schema changed, refresh the committed SDL first — this is the deliberate, reviewed step that does need a backend (endpoint from the repo-root .env):
pnpm --filter @oc-mui/query schema:refresh # updates src/schema.graphql — review the diff!
pnpm --filter @oc-mui/query codegenschema.graphql's header records which Opencast version it was taken from. Mind that Opencast's *MetadataInput types are per-organisation (readOnly catalog fields are absent from them) — refresh from an instance whose catalog configuration matches what the UI should support. If schema-input-fields.generated.ts changes, src/sortableFields.test.ts may need its pinned expectations updated — that test failing is the intended signal that the set of sortable/filterable fields shifted.
The schema's custom scalars are mapped explicitly in the codegen config — DateTime and Duration to string, Long to number, JSON to unknown — matching what the backend puts on the wire. Nullable result fields are generated as required-with-null (title: string | null), not optional; only @skip/@include/@defer make a field optional.
The config lives in src/codegen.ts; the refresh script in src/refresh-schema.mjs. typescript-operations ≥ 6 would re-emit every input type and enum an operation names, duplicating the typescript plugin's declarations in the same file — the config points its importSchemaTypesFrom back at the output file so it emits neither (see the comment there). Post-processing (src/fix-fetcher-import.mjs) unwraps the TypedDocumentString objects typescript-react-query ≥ 7 forces onto the document constants back into plain strings (a String subclass would break === and graphql-request's client.request() at runtime, and types nothing here), and annotates the generated hooks' return types explicitly so the emitted .d.ts never references tanstack's NoInfer — that reference only resolves on some @tanstack/react-query minors, and where it doesn't, every hook's data silently degrades to any for consumers (#354). test:consumer-types guards both halves.
Layer
Integration. Depends on @oc-mui/plugin-system, @oc-mui/utils. The Config Contract reader (definePluginConfig) ties it to @oc-mui/ui-config for the AppConfig shape.
See also
docs/reference/configuration.md— full Config layer model and the reader API.docs/reference/contracts.md— what's frozen.etc/query.api.md— committed API surface.
