SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
4.2 KB · 87 lines typescript
Raw Blame History
1import 'server-only';2import type { AdminOverview, ConnectorRunsPayload, CostsPayload, DataQualityPayload, RawRecord, RawRecordDetail, ReviewDecision, ReviewItem, AdminPaginated } from '@/components/admin/types';3import type { Envelope } from './types';4import { API_URL } from './api';56/**7 * Server-only client for the FastAPI admin endpoints. Adds `x-si-admin-token` from the server environment (the browser never8 * sees it) and never caches. Used by admin server components and by the Next route handlers under `app/api/admin/*`.9 */10const BASE = `${API_URL}/api/v1/admin`;1112export class AdminApiError extends Error {13  readonly status: number;14  readonly path: string;15  constructor(status: number, path: string, message: string) {16    super(message);17    this.name = 'AdminApiError';18    this.status = status;19    this.path = path;20  }21  get notFound(): boolean {22    return this.status === 404;23  }24}2526type Query = Record<string, string | number | boolean | null | undefined>;2728function qs(query?: Query): string {29  if (!query) return '';30  const p = new URLSearchParams();31  for (const [k, v] of Object.entries(query)) {32    if (v === undefined || v === null || v === '') continue;33    p.set(k, String(v));34  }35  const s = p.toString();36  return s ? `?${s}` : '';37}3839async function adminFetch<T>(path: string, opts: { method?: 'GET' | 'POST'; body?: unknown; query?: Query } = {}): Promise<T> {40  const token = process.env.SI_ADMIN_TOKEN ?? '';41  if (!token) throw new AdminApiError(500, path, 'SI_ADMIN_TOKEN is not configured on the server');42  const url = `${BASE}${path}${qs(opts.query)}`;43  let res: Response;44  try {45    res = await fetch(url, {46      method: opts.method ?? 'GET',47      cache: 'no-store',48      headers: { accept: 'application/json', 'x-si-admin-token': token, ...(opts.body !== undefined ? { 'content-type': 'application/json' } : {}) },49      body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,50    });51  } catch (e) {52    throw new AdminApiError(0, path, `Admin API unreachable: ${(e as Error).message}`);53  }54  if (!res.ok) {55    let detail = `Admin API ${res.status} on ${path}`;56    try {57      const body = (await res.json()) as { error?: { title?: string; detail?: string | null } };58      detail = body.error?.detail || body.error?.title || detail;59    } catch {60      /* non-JSON body */61    }62    throw new AdminApiError(res.status, path, detail);63  }64  return (await res.json()) as T;65}6667export const adminApi = {68  overview: () => adminFetch<Envelope<AdminOverview>>('/overview'),69  connectorRuns: (name: string, page = 1, pageSize = 50) => adminFetch<ConnectorRunsPayload>(`/connectors/${encodeURIComponent(name)}/runs`, { query: { page, page_size: pageSize } }),70  triggerRun: (name: string) => adminFetch<Envelope<{ queued: boolean; connector: string; note: string | null }>>(`/connectors/${encodeURIComponent(name)}/run`, { method: 'POST' }),71  setEnabled: (name: string, enabled: boolean) => adminFetch<Envelope<{ connector: string; enabled: boolean }>>(`/connectors/${encodeURIComponent(name)}/enabled`, { method: 'POST', body: { enabled } }),72  raw: (page = 1, connector?: string | null, pageSize = 50) => adminFetch<AdminPaginated<RawRecord>>('/raw', { query: { page, page_size: pageSize, connector } }),73  rawRecord: (id: string, maxBytes = 200_000) => adminFetch<Envelope<RawRecordDetail>>(`/raw/${encodeURIComponent(id)}`, { query: { max_bytes: maxBytes } }),74  dataQuality: (page = 1, flag?: string | null, pageSize = 50) => adminFetch<DataQualityPayload>('/data-quality', { query: { page, page_size: pageSize, flag } }),75  review: (page = 1, status = 'open', pageSize = 50) => adminFetch<AdminPaginated<ReviewItem>>('/review', { query: { page, page_size: pageSize, status } }),76  decide: (id: number, decision: ReviewDecision, by = 'admin') => adminFetch<Envelope<{ id: number; decision: string }>>(`/review/${id}`, { method: 'POST', body: { decision, by } }),77  costs: () => adminFetch<Envelope<CostsPayload>>('/costs'),78};7980export async function safeAdmin<T>(p: Promise<T>): Promise<{ data: T; error: null } | { data: null; error: string }> {81  try {82    return { data: await p, error: null };83  } catch (e) {84    return { data: null, error: e instanceof Error ? e.message : String(e) };85  }86}87