SPB Git
3.3 KB · 39 lines typescript
1import Papa from "papaparse";2import { eq } from "drizzle-orm";3import { db, schema } from "@/db";4import type { Property, RowOut } from "./types";5import { displayValue, parseValue, sortedProps, titleProp } from "./values";6import { createProperty, createRow, ensureDatabase } from "./service";7import { loadProps, loadRaw } from "./query";89export function exportCsv(props: Property[], rows: RowOut[], hidden: string[] = []): string {10  const cols = sortedProps(props).filter((p) => p.type === "title" || !hidden.includes(p.id));11  return "" + Papa.unparse({ fields: cols.map((p) => p.name), data: rows.map((r) => cols.map((p) => (p.type === "title" ? r.title : p.type === "file" ? (Array.isArray(r.values[p.id]) ? (r.values[p.id] as any[]).map((f) => f.url).join(", ") : "") : p.type === "date" || p.type === "created_time" || p.type === "last_edited_time" ? String(r.values[p.id] ?? "") : p.type === "number" ? String(r.values[p.id] ?? "") : displayValue(p, r.values[p.id])))) });12}13/** Import rows from CSV text; unknown columns become text properties. */14export async function importCsv(wsId: string, dbId: string, text: string) {15  const d = await ensureDatabase(wsId, dbId);16  const parsed = Papa.parse<Record<string, string>>(text.replace(/^/, ""), { header: true, skipEmptyLines: "greedy", transformHeader: (h) => h.trim() });17  if (!parsed.meta.fields?.length) throw new Error("CSV has no header row");18  let props = await loadProps(d.id); const tp = titleProp(props)!; const created: string[] = [];19  const byName = (n: string) => props.find((p) => p.name.toLowerCase() === n.toLowerCase());20  const titleCol = parsed.meta.fields.find((f) => f.toLowerCase() === tp.name.toLowerCase()) ?? parsed.meta.fields.find((f) => /^(name|title|titre|nom)$/i.test(f)) ?? parsed.meta.fields[0];21  const colMap = new Map<string, Property>();22  for (const f of parsed.meta.fields) {23    if (!f || f === titleCol) continue;24    let p = byName(f);25    if (!p) { p = await createProperty(wsId, d.id, { name: f, type: "text" }); props = [...props, p]; created.push(f); }26    colMap.set(f, p);27  }28  const relCache = new Map<string, Map<string, string>>();29  const relLookup = async (p: Property, s: string) => { const t = p.config.targetDatabaseId as string | undefined; if (!t) return []; let m = relCache.get(t); if (!m) { m = new Map((await loadRaw([eq(schema.databaseRows.databaseId, t)])).map((r) => [r.title.toLowerCase(), r.id])); relCache.set(t, m); } return s.split(/[,;|]/).map((x) => m!.get(x.trim().toLowerCase())).filter((x): x is string => !!x); };30  let n = 0; const touched = new Set<Property>();31  for (const rec of parsed.data) {32    const values: Record<string, unknown> = {};33    for (const [col, p] of colMap) { const raw = rec[col] ?? ""; if (!raw.trim()) continue; if (p.type === "relation") values[p.id] = await relLookup(p, raw); else if (["rollup", "formula", "created_time", "last_edited_time", "file"].includes(p.type)) continue; else { const before = JSON.stringify(p.config.options ?? []); values[p.id] = parseValue(p, raw); if (JSON.stringify(p.config.options ?? []) !== before) touched.add(p); } }34    await createRow(wsId, d.id, { title: (rec[titleCol] ?? "").trim(), values }); n++;35  }36  for (const p of touched) await db.update(schema.databaseProperties).set({ config: p.config }).where(eq(schema.databaseProperties.id, p.id));37  return { created: n, properties: created };38}39
Files · main
Menu

Theme