import { createId } from "@paralleldrive/cuid2" import type { ExamNode } from "../components/assembly/selected-question-list" /** * Normalize raw exam structure data into typed `ExamNode[]`. * * - Validates each node's shape at runtime (type guard pattern, no `as`). * - Ensures every node has a unique id (generates one if missing or duplicate). * - Recursively normalizes group children. * - Returns `[]` for non-array input. * * Used by the exam build page to convert persisted `exam.structure` (unknown * JSON from DB) into a typed tree before passing to ``. */ export function normalizeStructure(nodes: unknown): ExamNode[] { const seen = new Set() const isRecord = (v: unknown): v is Record => typeof v === "object" && v !== null const normalize = (raw: unknown[]): ExamNode[] => { return raw .map((n): ExamNode | null => { if (!isRecord(n)) return null const type = n.type if (type !== "group" && type !== "question") return null let id = typeof n.id === "string" && n.id.length > 0 ? n.id : createId() while (seen.has(id)) id = createId() seen.add(id) if (type === "group") { return { id, type: "group", title: typeof n.title === "string" ? n.title : undefined, children: normalize(Array.isArray(n.children) ? n.children : []), } satisfies ExamNode } if (typeof n.questionId !== "string" || n.questionId.length === 0) return null return { id, type: "question", questionId: n.questionId, score: typeof n.score === "number" ? n.score : undefined, } satisfies ExamNode }) .filter((n): n is ExamNode => n !== null) } if (!Array.isArray(nodes)) return [] return normalize(nodes) }