import { Exam } from "@/interfaces/exam"; import { diff, Diff } from "deep-diff"; const EXCLUDED_KEYS = new Set(["_id", "id", "createdAt", "createdBy", "entities", "isDiagnostic", "private", "access", "requiresApproval"]); const PATH_LABELS: Record = { parts: "Parts", exercises: "Exercises", userSolutions: "User Solutions", words: "Words", options: "Options", prompt: "Prompt", text: "Text", audio: "Audio", script: "Script", difficulty: "Difficulty", shuffle: "Shuffle", solutions: "Solutions", variant: "Variant", prefix: "Prefix", suffix: "Suffix", topic: "Topic", }; export function generateExamDifferences(oldExam: Exam, newExam: Exam): string[] { const differences = diff(oldExam, newExam) || []; return differences.map((change) => formatDifference(change)).filter(Boolean) as string[]; } function formatDifference(change: Diff): string | undefined { if (!change.path) { return; } if (change.path.some((segment) => EXCLUDED_KEYS.has(segment))) { return; } const pathString = pathToHumanReadable(change.path); switch (change.kind) { case "N": // New property/element return `• Added ${pathString} with value: ${formatValue(change.rhs)}\n`; case "D": // Deleted property/element return `• Removed ${pathString} which had value: ${formatValue(change.lhs)}\n`; case "E": // Edited property/element return `• Changed ${pathString} from ${formatValue(change.lhs)} to ${formatValue(change.rhs)}\n`; case "A": // Array change return formatArrayChange(change); default: return; } } function formatArrayChange(change: Diff): string | undefined { if (!change.path) return; if (change.path.some((segment) => EXCLUDED_KEYS.has(segment))) { return; } const pathString = pathToHumanReadable(change.path); const arrayChange = (change as any).item; const idx = (change as any).index; if (!arrayChange) return; switch (arrayChange.kind) { case "N": return `• Added an item at [#${idx + 1}] in ${pathString}: ${formatValue(arrayChange.rhs)}\n`; case "D": return `• Removed an item at [#${idx + 1}] in ${pathString}: ${formatValue(arrayChange.lhs)}\n`; case "E": return `• Edited an item at [#${idx + 1}] in ${pathString} from ${formatValue(arrayChange.lhs)} to ${formatValue(arrayChange.rhs)}\n`; case "A": return `• Complex array change at [#${idx + 1}] in ${pathString}: ${JSON.stringify(arrayChange)}\n`; default: return; } } function formatValue(value: any): string { if (value === null) return "null"; if (value === undefined) return "undefined"; if (typeof value === "object") { try { return JSON.stringify( value, (key, val) => { if (EXCLUDED_KEYS.has(key)) { return undefined; } return val; }, 2 // optional indentation for readability ); } catch { return String(value); } } return JSON.stringify(value); } /** * Convert an array of path segments into a friendlier string. * Example: * ["parts", 0, "exercises", 1, "prompt"] * becomes: * "Parts → [#1] → Exercises → [#2] → Prompt" */ function pathToHumanReadable(pathSegments: Array): string { return pathSegments .map((seg) => { if (typeof seg === "number") { return `[#${seg + 1}]`; } return PATH_LABELS[seg] ?? seg; }) .join(" → "); }