/** * TanStack Query hooks for the /v1/tenants/{id}/sources/* endpoints. * * The hooks are feature-scoped: callers (list, create-dialog, * detail-page) pull these and don't talk to fetchWithAuth * directly. That way the cache is shared across views and the * query keys are predictable. * * Cache key strategy: ['sources', 'list', tenantId, params] for * the list, ['sources', 'detail', tenantId, sourceId] for the * detail. tenantId is part of the key (not just the URL) so the * list cache for tenant A doesn't leak when the user navigates * to tenant B. */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ApiError, apiGet, apiSend } from '@/lib/api'; import type { CreateOrRotateResponse, CreateSourceInput, ListSourcesResponse, Source, UpdateSourceInput, } from './types'; export interface ListSourcesParams { q?: string; type?: string; status?: string; limit?: number; offset?: number; } const KEYS = { list: (tenantId: string | undefined, params: ListSourcesParams) => ['sources', 'list', tenantId, params] as const, detail: (tenantId: string | undefined, sourceId: string | undefined) => ['sources', 'detail', tenantId, sourceId] as const, }; function buildListQuery(tenantId: string, params: ListSourcesParams): string { const u = new URLSearchParams(); if (params.q) u.set('q', params.q); if (params.type) u.set('type', params.type); if (params.status) u.set('status', params.status); if (params.limit) u.set('limit', String(params.limit)); if (params.offset) u.set('offset', String(params.offset)); const s = u.toString(); return s ? `/v1/tenants/${tenantId}/sources?${s}` : `/v1/tenants/${tenantId}/sources`; } export function useSourcesList(tenantId: string | undefined, params: ListSourcesParams) { return useQuery({ queryKey: KEYS.list(tenantId, params), queryFn: () => apiGet(buildListQuery(tenantId as string, params)), enabled: Boolean(tenantId), staleTime: 15_000, }); } export function useSource(tenantId: string | undefined, sourceId: string | undefined) { return useQuery({ queryKey: KEYS.detail(tenantId, sourceId), queryFn: () => apiGet(`/v1/tenants/${tenantId}/sources/${sourceId}`), enabled: Boolean(tenantId) && Boolean(sourceId), }); } export function useCreateSource(tenantId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (input: CreateSourceInput) => apiSend('POST', `/v1/tenants/${tenantId}/sources`, input), onSuccess: () => { void qc.invalidateQueries({ queryKey: ['sources', 'list', tenantId] }); }, }); } export function useUpdateSource(tenantId: string, sourceId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (input: UpdateSourceInput) => apiSend('PATCH', `/v1/tenants/${tenantId}/sources/${sourceId}`, input), onSuccess: (source) => { qc.setQueryData(KEYS.detail(tenantId, sourceId), source); void qc.invalidateQueries({ queryKey: ['sources', 'list', tenantId] }); }, }); } export function useSetSourceStatus(tenantId: string, sourceId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (status: 'active' | 'suspended') => apiSend('POST', `/v1/tenants/${tenantId}/sources/${sourceId}/status`, { status, }), onSuccess: (source) => { qc.setQueryData(KEYS.detail(tenantId, sourceId), source); void qc.invalidateQueries({ queryKey: ['sources', 'list', tenantId] }); }, }); } export function useRotateSourceSecrets(tenantId: string, sourceId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: () => apiSend( 'POST', `/v1/tenants/${tenantId}/sources/${sourceId}/rotate-secrets`, {}, ), onSuccess: (resp) => { qc.setQueryData(KEYS.detail(tenantId, sourceId), resp.source); void qc.invalidateQueries({ queryKey: ['sources', 'list', tenantId] }); }, }); } export function getErrorMessage(err: unknown): string { if (err instanceof ApiError) { const body = err.body as { error?: string; message?: string } | null; return body?.message ?? body?.error ?? err.message; } if (err instanceof Error) return err.message; return 'Unknown error'; }