spb/note-spboucher-ai
Public
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