feat: add scoped context payload and rollbackable document changes

This commit is contained in:
Ruslan Bakiev
2026-02-21 16:27:09 +07:00
parent 052f37d0ec
commit fa1231df37
5 changed files with 678 additions and 13 deletions

View File

@@ -41,16 +41,28 @@ type DealSnapshotRow = {
summary: string | null;
};
type WorkspaceDocumentSnapshotRow = {
id: string;
teamId: string;
title: string;
type: string;
owner: string;
scope: string;
summary: string;
body: string;
};
export type SnapshotState = {
calendarById: Map<string, CalendarSnapshotRow>;
noteByContactId: Map<string, ContactNoteSnapshotRow>;
messageById: Map<string, MessageSnapshotRow>;
dealById: Map<string, DealSnapshotRow>;
documentById: Map<string, WorkspaceDocumentSnapshotRow>;
};
export type ChangeItem = {
id: string;
entity: "calendar_event" | "contact_note" | "message" | "deal";
entity: "calendar_event" | "contact_note" | "message" | "deal" | "workspace_document";
entityId: string | null;
action: "created" | "updated" | "deleted";
title: string;
@@ -65,7 +77,9 @@ type UndoOp =
| { kind: "delete_contact_message"; id: string }
| { kind: "restore_contact_message"; data: MessageSnapshotRow }
| { kind: "restore_contact_note"; contactId: string; content: string | null }
| { kind: "restore_deal"; id: string; stage: string; nextStep: string | null; summary: string | null };
| { kind: "restore_deal"; id: string; stage: string; nextStep: string | null; summary: string | null }
| { kind: "delete_workspace_document"; id: string }
| { kind: "restore_workspace_document"; data: WorkspaceDocumentSnapshotRow };
export type ChangeSet = {
id: string;
@@ -95,8 +109,12 @@ function toDealText(row: DealSnapshotRow) {
return `${row.title} (${row.contactName}) · ${row.stage}${row.nextStep ? ` · next: ${row.nextStep}` : ""}`;
}
function toWorkspaceDocumentText(row: WorkspaceDocumentSnapshotRow) {
return `${row.title} · ${row.type} · ${row.owner} · ${row.scope} · ${row.summary}`;
}
export async function captureSnapshot(prisma: PrismaClient, teamId: string): Promise<SnapshotState> {
const [calendar, notes, messages, deals] = await Promise.all([
const [calendar, notes, messages, deals, documents] = await Promise.all([
prisma.calendarEvent.findMany({
where: { teamId },
select: {
@@ -129,6 +147,20 @@ export async function captureSnapshot(prisma: PrismaClient, teamId: string): Pro
include: { contact: { select: { name: true } } },
take: 4000,
}),
prisma.workspaceDocument.findMany({
where: { teamId },
select: {
id: true,
teamId: true,
title: true,
type: true,
owner: true,
scope: true,
summary: true,
body: true,
},
take: 4000,
}),
]);
return {
@@ -188,6 +220,21 @@ export async function captureSnapshot(prisma: PrismaClient, teamId: string): Pro
},
]),
),
documentById: new Map(
documents.map((row) => [
row.id,
{
id: row.id,
teamId: row.teamId,
title: row.title,
type: row.type,
owner: row.owner,
scope: row.scope,
summary: row.summary,
body: row.body,
},
]),
),
};
}
@@ -326,6 +373,53 @@ export function buildChangeSet(before: SnapshotState, after: SnapshotState): Cha
}
}
for (const [id, row] of after.documentById) {
const prev = before.documentById.get(id);
if (!prev) {
pushItem({
entity: "workspace_document",
entityId: row.id,
action: "created",
title: `Document created: ${row.title}`,
before: "",
after: toWorkspaceDocumentText(row),
undo: [{ kind: "delete_workspace_document", id }],
});
continue;
}
if (
prev.title !== row.title ||
prev.type !== row.type ||
prev.owner !== row.owner ||
prev.scope !== row.scope ||
prev.summary !== row.summary ||
prev.body !== row.body
) {
pushItem({
entity: "workspace_document",
entityId: row.id,
action: "updated",
title: `Document updated: ${row.title}`,
before: toWorkspaceDocumentText(prev),
after: toWorkspaceDocumentText(row),
undo: [{ kind: "restore_workspace_document", data: prev }],
});
}
}
for (const [id, row] of before.documentById) {
if (after.documentById.has(id)) continue;
pushItem({
entity: "workspace_document",
entityId: row.id,
action: "deleted",
title: `Document deleted: ${row.title}`,
before: toWorkspaceDocumentText(row),
after: "",
undo: [{ kind: "restore_workspace_document", data: row }],
});
}
if (items.length === 0) return null;
const created = items.filter((x) => x.action === "created").length;
@@ -440,6 +534,38 @@ async function applyUndoOps(prisma: PrismaClient, teamId: string, undoOps: UndoO
summary: op.summary,
},
});
continue;
}
if (op.kind === "delete_workspace_document") {
await tx.workspaceDocument.deleteMany({ where: { id: op.id, teamId } });
continue;
}
if (op.kind === "restore_workspace_document") {
const row = op.data;
await tx.workspaceDocument.upsert({
where: { id: row.id },
update: {
teamId: row.teamId,
title: row.title,
type: row.type as any,
owner: row.owner,
scope: row.scope,
summary: row.summary,
body: row.body,
},
create: {
id: row.id,
teamId: row.teamId,
title: row.title,
type: row.type as any,
owner: row.owner,
scope: row.scope,
summary: row.summary,
body: row.body,
},
});
}
}
});