SPB Git
2.2 KB · 21 lines typescript
1/** Role-scoped export. scope=public → published public records only (safe to share). scope=all → everything incl. private/owner notes (owner only). format=json|csv */2import Papa from "papaparse";3import { requireUser, jsonError, HttpError } from "@/lib/auth/guard";4import { sql } from "@/db";5import { getToolRecord } from "@/lib/admin/tools";6export async function GET(req: Request) {7  try {8    const s = await requireUser(); const p = new URL(req.url).searchParams; const scope = p.get("scope") ?? "public"; const format = p.get("format") ?? "json";9    if (scope === "all" && s.role !== "owner") throw new HttpError(403, "Owner only");10    const ids = await sql<{ id: string }[]>`SELECT id FROM tools WHERE duplicate_of IS NULL ${scope === "all" ? sql`` : sql`AND visibility='public' AND editorial_status='published'`} ORDER BY slug`;11    const records = []; for (const r of ids) { const rec = await getToolRecord(r.id); if (!rec) continue; const { id: _i, quality_score, link_state, checked_at, published_at, updated_at, duplicate_of: _d, ...rest } = rec; records.push(scope === "all" ? { ...rest, _meta: { quality_score, link_state, checked_at, published_at, updated_at } } : rest); }12    if (scope === "all") { const notes = await sql<any[]>`SELECT t.slug, o.* FROM owner_notes o JOIN tools t ON t.id=o.tool_id`; (records as any).push({ _owner_notes: notes }); }13    const stamp = new Date().toISOString().slice(0, 10);14    if (format === "csv") {15      const flat = records.filter((r: any) => !r._owner_notes).map((r: any) => Object.fromEntries(Object.entries(r).map(([k, v]) => [k, Array.isArray(v) ? (k === "categories" ? v.map((c: any) => c.slug).join("|") : k === "sources" ? v.map((x: any) => `${x.url}^${x.type}`).join("|") : v.join("|")) : typeof v === "object" && v ? (k === "vendor" ? (v as any).name : JSON.stringify(v)) : v])));16      return new Response(Papa.unparse(flat), { headers: { "content-type": "text/csv; charset=utf-8", "content-disposition": `attachment; filename="tools-${scope}-${stamp}.csv"` } });17    }18    return new Response(JSON.stringify(records, null, 2), { headers: { "content-type": "application/json", "content-disposition": `attachment; filename="tools-${scope}-${stamp}.json"` } });19  } catch (e) { return jsonError(e); }20}21
Files · laptop-initial-20261102
Menu

Theme