import { db } from "../../../db/index"; import { topics, userProgress } from "../../../db/schema"; import { eq, and, sql } from "drizzle-orm"; import { randomUUID } from "crypto"; export default defineEventHandler(async (event) => { const id = getRouterParam(event, "id")!; const topic = await db.query.topics.findFirst({ where: eq(topics.id, id) }); if (!topic) throw createError({ statusCode: 404, message: "Topic not found" }); const body = await readBody(event); const { lessonComplete, quizScore, tookBranches, branchCount } = body ?? {}; // manual validation — no zod if (lessonComplete !== undefined && typeof lessonComplete !== "boolean") { throw createError({ statusCode: 400, message: "lessonComplete must be a boolean" }); } if (quizScore !== undefined && quizScore !== null) { if (!Number.isInteger(quizScore)) throw createError({ statusCode: 400, message: "quizScore must be an integer" }); } if (tookBranches !== undefined && typeof tookBranches !== "boolean") { throw createError({ statusCode: 400, message: "tookBranches must be a boolean" }); } if (branchCount !== undefined && !Number.isInteger(branchCount)) { throw createError({ statusCode: 400, message: "branchCount must be an integer" }); } // NOTE: this requires a UNIQUE constraint on (course_id, topic_id) in user_progress — the migration adds this await db .insert(userProgress) .values({ id: randomUUID(), courseId: topic.courseId, topicId: id, lessonComplete: lessonComplete ?? false, quizScore: quizScore ?? null, tookBranches: tookBranches ?? false, branchCount: branchCount ?? 0, updatedAt: sql`datetime('now')`, }) .onConflictDoUpdate({ target: [userProgress.courseId, userProgress.topicId], set: { lessonComplete: lessonComplete ?? sql`excluded.lesson_complete`, quizScore: quizScore !== undefined ? quizScore : sql`excluded.quiz_score`, tookBranches: tookBranches ?? sql`excluded.took_branches`, branchCount: branchCount ?? sql`excluded.branch_count`, updatedAt: sql`datetime('now')`, }, }); return { ok: true }; });