import fs from "node:fs/promises"; import path from "node:path"; import { prisma } from "../utils/prisma"; import { datasetRoot } from "./paths"; type ExportMeta = { exportedAt: string; version: number; }; async function ensureDir(p: string) { await fs.mkdir(p, { recursive: true }); } async function writeJson(p: string, value: unknown) { await fs.writeFile(p, JSON.stringify(value, null, 2) + "\n", "utf8"); } function jsonlLine(value: unknown) { return JSON.stringify(value) + "\n"; } export async function exportDatasetFromPrisma() { throw new Error("exportDatasetFromPrisma now requires { teamId, userId }"); } export async function exportDatasetFromPrismaFor(input: { teamId: string; userId: string }) { const root = datasetRoot(input); const tmp = root + ".tmp"; await fs.rm(tmp, { recursive: true, force: true }); await ensureDir(tmp); const contactsDir = path.join(tmp, "contacts"); const notesDir = path.join(tmp, "notes"); const messagesDir = path.join(tmp, "messages"); const eventsDir = path.join(tmp, "events"); const indexDir = path.join(tmp, "index"); await Promise.all([ ensureDir(contactsDir), ensureDir(notesDir), ensureDir(messagesDir), ensureDir(eventsDir), ensureDir(indexDir), ]); const contacts = await prisma.contact.findMany({ where: { teamId: input.teamId }, orderBy: { updatedAt: "desc" }, include: { note: { select: { content: true, updatedAt: true } }, messages: { select: { kind: true, direction: true, channel: true, content: true, durationSec: true, transcriptJson: true, occurredAt: true, }, orderBy: { occurredAt: "asc" }, }, events: { select: { title: true, startsAt: true, endsAt: true, status: true, note: true }, orderBy: { startsAt: "asc" }, }, }, take: 5000, }); const contactIndex = []; for (const c of contacts) { const contactFile = path.join(contactsDir, `${c.id}.json`); await writeJson(contactFile, { id: c.id, teamId: c.teamId, name: c.name, company: c.company ?? null, country: c.country ?? null, location: c.location ?? null, avatarUrl: c.avatarUrl ?? null, email: c.email ?? null, phone: c.phone ?? null, createdAt: c.createdAt, updatedAt: c.updatedAt, }); const noteFile = path.join(notesDir, `${c.id}.md`); await fs.writeFile( noteFile, (c.note?.content?.trim() ? c.note.content.trim() : "") + "\n", "utf8", ); const msgFile = path.join(messagesDir, `${c.id}.jsonl`); const msgLines = c.messages.map((m) => jsonlLine({ kind: m.kind, direction: m.direction, channel: m.channel, occurredAt: m.occurredAt, content: m.content, durationSec: m.durationSec ?? null, transcript: m.transcriptJson ?? null, }), ); await fs.writeFile(msgFile, msgLines.join(""), "utf8"); const evFile = path.join(eventsDir, `${c.id}.jsonl`); const evLines = c.events.map((e) => jsonlLine({ title: e.title, startsAt: e.startsAt, endsAt: e.endsAt, status: e.status ?? null, note: e.note ?? null, }), ); await fs.writeFile(evFile, evLines.join(""), "utf8"); const lastMessageAt = c.messages.length ? c.messages[c.messages.length - 1].occurredAt : null; const nextEventAt = c.events.find((e) => new Date(e.startsAt).getTime() >= Date.now())?.startsAt ?? null; contactIndex.push({ id: c.id, name: c.name, company: c.company ?? null, lastMessageAt, nextEventAt, updatedAt: c.updatedAt, }); } await writeJson(path.join(indexDir, "contacts.json"), contactIndex); const meta: ExportMeta = { exportedAt: new Date().toISOString(), version: 1 }; await writeJson(path.join(tmp, "meta.json"), meta); await ensureDir(path.dirname(root)); await fs.rm(root, { recursive: true, force: true }); await fs.rename(tmp, root); } export async function ensureDataset(input: { teamId: string; userId: string }) { const root = datasetRoot(input); try { const metaPath = path.join(root, "meta.json"); await fs.access(metaPath); return; } catch { // fallthrough } await exportDatasetFromPrismaFor(input); }