import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { api } from "./client"; import type { components } from "./schema"; export class HttpError extends Error { constructor(public readonly status: number) { super(`HTTP ${status}`); this.name = "HttpError"; } } type UserView = components["schemas"]["UserView"]; type LoginRequest = components["schemas"]["LoginRequest"]; export function useMe() { return useQuery({ queryKey: ["me"], queryFn: async (): Promise => { const { data, response } = await api.GET("/api/admin/me"); if (response.status === 401) return null; if (!data) throw new Error("failed to load session"); return data; }, retry: false, }); } export function useObjectsPage(limit: number, offset: number) { return useQuery({ queryKey: ["objects", { limit, offset }], queryFn: async () => { const { data, error } = await api.GET("/api/admin/objects", { params: { query: { limit, offset } }, }); if (error || !data) throw new Error("failed to load objects"); return data; }, }); } export function useObject(id: string) { return useQuery({ queryKey: ["object", id], queryFn: async () => { const { data, response } = await api.GET("/api/admin/objects/{id}", { params: { path: { id } }, }); if (response.status === 404) return null; if (!data) throw new Error("failed to load object"); return data; }, // A 404 resolves to null rather than erroring, so don't retry it. retry: false, }); } export function useFieldDefinitions() { return useQuery({ queryKey: ["field-definitions"], queryFn: async () => { const { data, error } = await api.GET("/api/admin/field-definitions"); if (error || !data) throw new Error("failed to load field definitions"); return data; }, staleTime: 5 * 60 * 1000, }); } export function useLogin() { const qc = useQueryClient(); return useMutation({ mutationFn: async (body: LoginRequest) => { const { response } = await api.POST("/api/admin/login", { body }); if (response.status !== 204) { throw new Error(response.status === 401 ? "invalid" : "network"); } }, onSuccess: () => qc.invalidateQueries({ queryKey: ["me"] }), }); } export function useLogout() { const qc = useQueryClient(); return useMutation({ mutationFn: async () => { await api.POST("/api/admin/logout"); }, onSuccess: () => qc.setQueryData(["me"], null), }); } type ObjectCreateRequest = components["schemas"]["ObjectCreateRequest"]; type ObjectUpdateRequest = components["schemas"]["ObjectUpdateRequest"]; export function useTerms(vocabularyId: string | null | undefined) { return useQuery({ queryKey: ["terms", vocabularyId], enabled: !!vocabularyId, queryFn: async () => { const { data, error } = await api.GET("/api/admin/vocabularies/{id}/terms", { params: { path: { id: vocabularyId! } }, }); if (error || !data) throw new Error("failed to load terms"); return data; }, staleTime: 5 * 60 * 1000, }); } export function useAuthorities(kind: string | null | undefined) { return useQuery({ queryKey: ["authorities", kind], enabled: !!kind, queryFn: async () => { const { data, error } = await api.GET("/api/admin/authorities", { params: { query: { kind: kind! } }, }); if (error || !data) throw new Error("failed to load authorities"); return data; }, staleTime: 5 * 60 * 1000, }); } export function useCreateObject() { const qc = useQueryClient(); return useMutation({ mutationFn: async (body: ObjectCreateRequest) => { const { data, error } = await api.POST("/api/admin/objects", { body }); if (error || !data) throw new Error("create failed"); return data; }, onSuccess: () => qc.invalidateQueries({ queryKey: ["objects"] }), }); } export function useUpdateObject() { const qc = useQueryClient(); return useMutation({ mutationFn: async ({ id, body }: { id: string; body: ObjectUpdateRequest }) => { const { response } = await api.PUT("/api/admin/objects/{id}", { params: { path: { id } }, body, }); if (response.status !== 204) throw new Error("update failed"); }, onSuccess: (_d, { id }) => { void qc.invalidateQueries({ queryKey: ["objects"] }); void qc.invalidateQueries({ queryKey: ["object", id] }); }, }); } export function useSetFields() { const qc = useQueryClient(); return useMutation({ mutationFn: async ({ id, fields }: { id: string; fields: Record }) => { const { response } = await api.PUT("/api/admin/objects/{id}/fields", { params: { path: { id } }, body: fields as Record, }); if (response.status !== 204) throw new Error("set fields failed"); }, onSuccess: (_d, { id }) => { void qc.invalidateQueries({ queryKey: ["object", id] }); }, }); } export function useDeleteObject() { const qc = useQueryClient(); return useMutation({ mutationFn: async (id: string) => { const { response } = await api.DELETE("/api/admin/objects/{id}", { params: { path: { id } }, }); if (response.status !== 204) throw new Error("delete failed"); }, onSuccess: () => qc.invalidateQueries({ queryKey: ["objects"] }), }); } type NewVocabularyRequest = components["schemas"]["NewVocabularyRequest"]; type LabelInput = components["schemas"]["LabelInput"]; export function useVocabularies() { return useQuery({ queryKey: ["vocabularies"], queryFn: async () => { const { data, error } = await api.GET("/api/admin/vocabularies"); if (error || !data) throw new Error("failed to load vocabularies"); return data; }, staleTime: 5 * 60 * 1000, }); } export function useCreateVocabulary() { const qc = useQueryClient(); return useMutation({ mutationFn: async (body: NewVocabularyRequest) => { const { data, error } = await api.POST("/api/admin/vocabularies", { body }); if (error || !data) throw new Error("create vocabulary failed"); return data; }, onSuccess: () => qc.invalidateQueries({ queryKey: ["vocabularies"] }), }); } export function useAddTerm() { const qc = useQueryClient(); return useMutation({ mutationFn: async ({ vocabularyId, external_uri, labels, }: { vocabularyId: string; external_uri: string | null; labels: LabelInput[]; }) => { const { response } = await api.POST("/api/admin/vocabularies/{id}/terms", { params: { path: { id: vocabularyId } }, body: { external_uri, labels }, }); if (response.status !== 201) throw new Error("add term failed"); }, onSuccess: (_result, { vocabularyId }) => qc.invalidateQueries({ queryKey: ["terms", vocabularyId] }), }); } export function useCreateAuthority() { const qc = useQueryClient(); return useMutation({ mutationFn: async ({ kind, external_uri, labels, }: { kind: string; external_uri: string | null; labels: LabelInput[]; }) => { const { response } = await api.POST("/api/admin/authorities", { body: { kind, external_uri, labels }, }); if (response.status !== 201) throw new Error("create authority failed"); }, onSuccess: (_result, { kind }) => qc.invalidateQueries({ queryKey: ["authorities", kind] }), }); } const SEARCH_PAGE = 20; export function useSearch(q: string, visibility: string | null) { const term = q.trim(); return useInfiniteQuery({ queryKey: ["search", term, visibility], enabled: term.length > 0, initialPageParam: 0, queryFn: async ({ pageParam }) => { const { data, error, response } = await api.GET("/api/admin/search", { params: { query: { q: term, ...(visibility ? { visibility } : {}), offset: pageParam, limit: SEARCH_PAGE, }, }, }); if (error || !data) throw new HttpError(response.status); return data; }, placeholderData: keepPreviousData, getNextPageParam: (lastPage, allPages) => { const loaded = allPages.reduce((n, page) => n + page.hits.length, 0); return loaded < lastPage.estimated_total ? loaded : undefined; }, }); } type NewFieldDefinitionRequest = components["schemas"]["NewFieldDefinitionRequest"]; export function useCreateFieldDefinition() { const qc = useQueryClient(); return useMutation({ mutationFn: async (body: NewFieldDefinitionRequest) => { const { data, response } = await api.POST("/api/admin/field-definitions", { body }); if (response.status !== 201 || !data) throw new Error("failed to create field definition"); return data; }, onSuccess: () => qc.invalidateQueries({ queryKey: ["field-definitions"] }), }); } type Visibility = "draft" | "internal" | "public"; /** Error carrying the HTTP status so callers can branch 422-gate vs 409-illegal. */ export class VisibilityError extends Error { constructor(public status: number) { super(`visibility change failed (${status})`); this.name = "VisibilityError"; } } export function useSetVisibility() { const qc = useQueryClient(); return useMutation({ mutationFn: async ({ id, visibility }: { id: string; visibility: Visibility }) => { const { response } = await api.POST("/api/admin/objects/{id}/visibility", { params: { path: { id } }, body: { visibility }, }); if (response.status !== 204) throw new VisibilityError(response.status); }, onSuccess: (_result, { id }) => { void qc.invalidateQueries({ queryKey: ["object", id] }); void qc.invalidateQueries({ queryKey: ["objects"] }); }, }); }