api.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. /**
  2. * TanStack Query hooks for the /v1/tenants/{id}/sources/* endpoints.
  3. *
  4. * The hooks are feature-scoped: callers (list, create-dialog,
  5. * detail-page) pull these and don't talk to fetchWithAuth
  6. * directly. That way the cache is shared across views and the
  7. * query keys are predictable.
  8. *
  9. * Cache key strategy: ['sources', 'list', tenantId, params] for
  10. * the list, ['sources', 'detail', tenantId, sourceId] for the
  11. * detail. tenantId is part of the key (not just the URL) so the
  12. * list cache for tenant A doesn't leak when the user navigates
  13. * to tenant B.
  14. */
  15. import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
  16. import { ApiError, apiGet, apiSend } from '@/lib/api';
  17. import type {
  18. CreateOrRotateResponse,
  19. CreateSourceInput,
  20. ListSourcesResponse,
  21. Source,
  22. UpdateSourceInput,
  23. } from './types';
  24. export interface ListSourcesParams {
  25. q?: string;
  26. type?: string;
  27. status?: string;
  28. limit?: number;
  29. offset?: number;
  30. }
  31. const KEYS = {
  32. list: (tenantId: string | undefined, params: ListSourcesParams) =>
  33. ['sources', 'list', tenantId, params] as const,
  34. detail: (tenantId: string | undefined, sourceId: string | undefined) =>
  35. ['sources', 'detail', tenantId, sourceId] as const,
  36. };
  37. function buildListQuery(tenantId: string, params: ListSourcesParams): string {
  38. const u = new URLSearchParams();
  39. if (params.q) u.set('q', params.q);
  40. if (params.type) u.set('type', params.type);
  41. if (params.status) u.set('status', params.status);
  42. if (params.limit) u.set('limit', String(params.limit));
  43. if (params.offset) u.set('offset', String(params.offset));
  44. const s = u.toString();
  45. return s
  46. ? `/v1/tenants/${tenantId}/sources?${s}`
  47. : `/v1/tenants/${tenantId}/sources`;
  48. }
  49. export function useSourcesList(tenantId: string | undefined, params: ListSourcesParams) {
  50. return useQuery({
  51. queryKey: KEYS.list(tenantId, params),
  52. queryFn: () => apiGet<ListSourcesResponse>(buildListQuery(tenantId as string, params)),
  53. enabled: Boolean(tenantId),
  54. staleTime: 15_000,
  55. });
  56. }
  57. export function useSource(tenantId: string | undefined, sourceId: string | undefined) {
  58. return useQuery({
  59. queryKey: KEYS.detail(tenantId, sourceId),
  60. queryFn: () => apiGet<Source>(`/v1/tenants/${tenantId}/sources/${sourceId}`),
  61. enabled: Boolean(tenantId) && Boolean(sourceId),
  62. });
  63. }
  64. export function useCreateSource(tenantId: string) {
  65. const qc = useQueryClient();
  66. return useMutation({
  67. mutationFn: (input: CreateSourceInput) =>
  68. apiSend<CreateOrRotateResponse>('POST', `/v1/tenants/${tenantId}/sources`, input),
  69. onSuccess: () => {
  70. void qc.invalidateQueries({ queryKey: ['sources', 'list', tenantId] });
  71. },
  72. });
  73. }
  74. export function useUpdateSource(tenantId: string, sourceId: string) {
  75. const qc = useQueryClient();
  76. return useMutation({
  77. mutationFn: (input: UpdateSourceInput) =>
  78. apiSend<Source>('PATCH', `/v1/tenants/${tenantId}/sources/${sourceId}`, input),
  79. onSuccess: (source) => {
  80. qc.setQueryData(KEYS.detail(tenantId, sourceId), source);
  81. void qc.invalidateQueries({ queryKey: ['sources', 'list', tenantId] });
  82. },
  83. });
  84. }
  85. export function useSetSourceStatus(tenantId: string, sourceId: string) {
  86. const qc = useQueryClient();
  87. return useMutation({
  88. mutationFn: (status: 'active' | 'suspended') =>
  89. apiSend<Source>('POST', `/v1/tenants/${tenantId}/sources/${sourceId}/status`, {
  90. status,
  91. }),
  92. onSuccess: (source) => {
  93. qc.setQueryData(KEYS.detail(tenantId, sourceId), source);
  94. void qc.invalidateQueries({ queryKey: ['sources', 'list', tenantId] });
  95. },
  96. });
  97. }
  98. export function useRotateSourceSecrets(tenantId: string, sourceId: string) {
  99. const qc = useQueryClient();
  100. return useMutation({
  101. mutationFn: () =>
  102. apiSend<CreateOrRotateResponse>(
  103. 'POST',
  104. `/v1/tenants/${tenantId}/sources/${sourceId}/rotate-secrets`,
  105. {},
  106. ),
  107. onSuccess: (resp) => {
  108. qc.setQueryData(KEYS.detail(tenantId, sourceId), resp.source);
  109. void qc.invalidateQueries({ queryKey: ['sources', 'list', tenantId] });
  110. },
  111. });
  112. }
  113. export function getErrorMessage(err: unknown): string {
  114. if (err instanceof ApiError) {
  115. const body = err.body as { error?: string; message?: string } | null;
  116. return body?.message ?? body?.error ?? err.message;
  117. }
  118. if (err instanceof Error) return err.message;
  119. return 'Unknown error';
  120. }