157 lines
4.3 KiB
TypeScript
157 lines
4.3 KiB
TypeScript
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, isArchived: 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,
|
|
isArchived: e.isArchived,
|
|
note: e.note ?? null,
|
|
}),
|
|
);
|
|
await fs.writeFile(evFile, evLines.join(""), "utf8");
|
|
|
|
const lastMessageAt = c.messages.at(-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);
|
|
}
|