SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
6.4 KB · 119 lines typescript
Raw Blame History
1'use client';2/**3 * Browser-side fetches: same origin `/api/v1/*` (Next rewrite → FastAPI). Never import the server `api.ts` in client code.4 * Owner and admin calls attach their token headers (`lib/owner.ts`, `lib/admin.ts`).5 */6import type {7  Alert,8  AlertCondition,9  AlertDelivery,10  AdminCompany,11  AdminConnector,12  AdminCosts,13  AdminFailure,14  AdminLlmJob,15  AdminOverview,16  AdminQuality,17  AdminQueueItem,18  AdminReview,19  AdminSensor,20  AskPayload,21  CompanyCard,22  Event,23  EventDetail,24  Page,25  SearchPayload,26  Suggestion,27  WatchlistPayload,28} from './types';2930export class ClientApiError extends Error {31  readonly status: number;32  readonly detail: string | null;33  constructor(status: number, path: string, detail: string | null = null) {34    super(detail ?? `API ${status} on ${path}`);35    this.name = 'ClientApiError';36    this.status = status;37    this.detail = detail;38  }39}4041type Init = { method?: string; body?: unknown; headers?: Record<string, string>; signal?: AbortSignal };4243async function call<T>(path: string, init: Init = {}): Promise<T> {44  const headers: Record<string, string> = { accept: 'application/json', ...(init.headers ?? {}) };45  if (init.body !== undefined) headers['content-type'] = 'application/json';46  const res = await fetch(`/api/v1${path}`, { method: init.method ?? 'GET', headers, body: init.body === undefined ? undefined : JSON.stringify(init.body), signal: init.signal, cache: 'no-store' });47  if (!res.ok) {48    let detail: string | null = null;49    try {50      const b = (await res.json()) as { detail?: unknown };51      detail = typeof b.detail === 'string' ? b.detail : null;52    } catch {53      /* ignore */54    }55    throw new ClientApiError(res.status, path, detail);56  }57  if (res.status === 204) return undefined as T;58  return (await res.json()) as T;59}6061const enc = encodeURIComponent;6263export function buildQs(q: Record<string, string | number | boolean | null | undefined>): string {64  const p = new URLSearchParams();65  for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== '') p.set(k, typeof v === 'boolean' ? (v ? '1' : '0') : String(v));66  const s = p.toString();67  return s ? `?${s}` : '';68}6970export const clientApi = {71  suggest: (q: string, signal?: AbortSignal) => call<{ items: Suggestion[] }>(`/search/suggest?q=${enc(q)}`, { signal }),72  search: (q: string, limit = 8, signal?: AbortSignal) => call<SearchPayload>(`/search?q=${enc(q)}&limit=${limit}`, { signal }),73  ask: (q: string, signal?: AbortSignal) => call<AskPayload>(`/ask?q=${enc(q)}`, { signal }),74  live: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => call<{ items: Event[] } | Event[]>(`/live${buildQs(q)}`, { signal }),75  event: (id: string, signal?: AbortSignal) => call<EventDetail>(`/events/${enc(id)}`, { signal }),76  companies: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => call<Page<CompanyCard>>(`/companies${buildQs(q)}`, { signal }),77};7879/** Watchlist + alerts with the owner token. */80export function ownerApi(token: string) {81  const h = { 'X-CA-Owner-Token': token };82  return {83    watchlist: (signal?: AbortSignal) => call<WatchlistPayload>('/watchlist', { headers: h, signal }),84    watch: (slug: string) => call<unknown>('/watchlist', { method: 'POST', body: { company: slug }, headers: h }),85    unwatch: (slug: string) => call<unknown>(`/watchlist/${enc(slug)}`, { method: 'DELETE', headers: h }),86    alerts: (signal?: AbortSignal) => call<{ items: Alert[] }>('/alerts', { headers: h, signal }),87    createAlert: (body: { name: string; company?: string; condition: AlertCondition; channel: 'web' | 'webhook'; target?: string }) => call<Alert>('/alerts', { method: 'POST', body, headers: h }),88    deleteAlert: (id: string) => call<unknown>(`/alerts/${enc(id)}`, { method: 'DELETE', headers: h }),89    deliveries: (limit = 50, signal?: AbortSignal) => call<{ items: AlertDelivery[] }>(`/alerts/deliveries?limit=${limit}`, { headers: h, signal }),90  };91}9293/** Admin console with the admin token. */94export function adminApi(token: string) {95  const h = { 'X-CA-Admin-Token': token };96  const g = <T>(path: string, signal?: AbortSignal) => call<T>(path, { headers: h, signal });97  const post = <T>(path: string, body?: unknown) => call<T>(path, { method: 'POST', body: body ?? {}, headers: h });98  return {99    overview: (signal?: AbortSignal) => g<AdminOverview>('/admin/overview', signal),100    connectors: (signal?: AbortSignal) => g<{ items: AdminConnector[] }>('/admin/connectors', signal),101    sensors: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<Page<AdminSensor>>(`/admin/sensors${buildQs(q)}`, signal),102    sensorAction: (id: string, action: string, body?: unknown) => post<unknown>(`/admin/sensors/${enc(id)}/${action}`, body),103    companies: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<Page<AdminCompany>>(`/admin/companies${buildQs(q)}`, signal),104    createCompany: (body: { website: string; display_name?: string; country?: string; industries?: string[] }) => post<unknown>('/admin/companies', body),105    rediscover: (slug: string) => post<unknown>(`/admin/companies/${enc(slug)}/rediscover`),106    failures: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<Page<AdminFailure>>(`/admin/failures${buildQs(q)}`, signal),107    queue: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<{ items: AdminQueueItem[]; counts?: Record<string, number> } | Page<AdminQueueItem>>(`/admin/queue${buildQs(q)}`, signal),108    requeueDead: () => post<unknown>('/admin/queue/requeue-dead'),109    llm: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<Page<AdminLlmJob>>(`/admin/llm${buildQs(q)}`, signal),110    reviews: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<{ items: AdminReview[] } | Page<AdminReview>>(`/admin/reviews${buildQs(q)}`, signal),111    resolveReview: (id: string, resolution: 'accepted' | 'rejected', note?: string) => post<unknown>(`/admin/reviews/${enc(id)}`, { resolution, note }),112    retractEvent: (id: string, reason: string) => post<unknown>(`/admin/events/${enc(id)}/retract`, { reason }),113    restoreEvent: (id: string) => post<unknown>(`/admin/events/${enc(id)}/restore`),114    quality: (signal?: AbortSignal) => g<AdminQuality>('/admin/quality', signal),115    costs: (days = 30, signal?: AbortSignal) => g<AdminCosts>(`/admin/costs?days=${days}`, signal),116    clearCache: () => post<unknown>('/admin/cache/clear'),117  };118}119