SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
3.9 KB · 88 lines typescript
Raw Blame History
1"use server";23import { revalidatePath } from "next/cache";4import { crawlCreateSchema, mapCreateSchema } from "@fetcha/core";5import { getWorkspace } from "@/lib/session";6import { internalApi, InternalApiError, type CrawlJob, type MapResult } from "@/lib/api";78export interface CrawlActionError {9  code: string;10  message: string;11  requestId: string | null;12  details?: Record<string, unknown>;13}1415export type CrawlActionResult<T> = { ok: true; data: T } | { ok: false; error: CrawlActionError };1617function invalid(issues: Array<{ path: string; message: string }>): CrawlActionError {18  const first = issues[0];19  return {20    code: "INVALID_REQUEST",21    message: first ? `${first.path ? `${first.path}: ` : ""}${first.message}` : "The request is invalid.",22    requestId: null,23    details: { issues },24  };25}2627function fromError(e: unknown, fallback: string): CrawlActionError {28  if (e instanceof InternalApiError) return { code: e.code, message: e.message, requestId: e.requestId, details: e.details };29  console.error("[crawls] unexpected error", e);30  return { code: "INTERNAL_ERROR", message: fallback, requestId: null };31}3233/** Strip empty strings / undefined so optional fields do not trip the strict schema. */34function clean(input: unknown): Record<string, unknown> {35  if (!input || typeof input !== "object") return {};36  return Object.fromEntries(Object.entries(input as Record<string, unknown>).filter(([, v]) => v !== undefined && v !== "" && v !== null));37}3839/**40 * Start a crawl job for the current project. Validated with the shared `crawlCreateSchema`; the API41 * enforces plan limits (pages per job, concurrent jobs). Never throws to the client.42 */43export async function createCrawl(input: unknown): Promise<CrawlActionResult<CrawlJob>> {44  const ws = await getWorkspace();45  try {46    const parsed = crawlCreateSchema.safeParse(clean(input));47    if (!parsed.success) return { ok: false, error: invalid(parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }))) };48    const job = await internalApi.createCrawl(ws.project.id, ws.user.id, parsed.data);49    if (!job || typeof job !== "object" || typeof job.id !== "string") {50      return { ok: false, error: { code: "INTERNAL_ERROR", message: "The API returned an unexpected response.", requestId: null } };51    }52    revalidatePath("/dashboard/crawls");53    return { ok: true, data: job };54  } catch (e) {55    return { ok: false, error: fromError(e, "Could not start the crawl. Please try again.") };56  }57}5859/** Cancel a queued or running crawl job. */60export async function cancelCrawl(id: string): Promise<CrawlActionResult<{ id: string; status: "cancelled" }>> {61  const ws = await getWorkspace();62  if (!/^crawl_[A-Za-z0-9]{4,64}$/.test(id)) return { ok: false, error: { code: "INVALID_REQUEST", message: "Invalid crawl id.", requestId: null } };63  try {64    const res = await internalApi.cancelCrawl(ws.project.id, ws.user.id, id);65    revalidatePath("/dashboard/crawls");66    revalidatePath(`/dashboard/crawls/${id}`);67    return { ok: true, data: res };68  } catch (e) {69    return { ok: false, error: fromError(e, "Could not cancel the crawl. Please try again.") };70  }71}7273/** Discover the URLs of a site (sitemap + links), synchronously. */74export async function runMap(input: unknown): Promise<CrawlActionResult<MapResult>> {75  const ws = await getWorkspace();76  try {77    const parsed = mapCreateSchema.safeParse(clean(input));78    if (!parsed.success) return { ok: false, error: invalid(parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }))) };79    const result = await internalApi.mapSite(ws.project.id, ws.user.id, parsed.data);80    if (!result || typeof result !== "object" || !Array.isArray(result.urls)) {81      return { ok: false, error: { code: "INTERNAL_ERROR", message: "The API returned an unexpected response.", requestId: null } };82    }83    return { ok: true, data: result };84  } catch (e) {85    return { ok: false, error: fromError(e, "Could not map the site. Please try again.") };86  }87}88