initialize project with basic structure and dependencies
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { db } from "../../../db/index";
|
||||
import { courses } from "../../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { generateCourseInBackground } from "../../../utils/generateCourse";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id")!;
|
||||
|
||||
const course = await db.query.courses.findFirst({ where: eq(courses.id, id) });
|
||||
if (!course) throw createError({ statusCode: 404, message: "Course not found" });
|
||||
|
||||
await db.update(courses).set({ status: "processing", stage: "parsing_pdfs" }).where(eq(courses.id, id));
|
||||
|
||||
// fire and forget — response returns immediately
|
||||
console.log(`[revisione:${id.slice(0, 8)}] background generation triggered for "${course.title}"`);
|
||||
generateCourseInBackground(id).catch(() => {});
|
||||
|
||||
setResponseStatus(event, 202);
|
||||
return { message: "Course generation started" };
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { db } from "../../../db/index";
|
||||
import { courses, topics, userProgress, lessons } from "../../../db/schema";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id")!;
|
||||
|
||||
const course = await db.query.courses.findFirst({
|
||||
where: eq(courses.id, id),
|
||||
});
|
||||
|
||||
if (!course) throw createError({ statusCode: 404, message: "Course not found" });
|
||||
|
||||
const topicRows = await db.query.topics.findMany({
|
||||
where: eq(topics.courseId, id),
|
||||
orderBy: (t, { asc }) => asc(t.order),
|
||||
});
|
||||
|
||||
const progressRows = await db.query.userProgress.findMany({
|
||||
where: eq(userProgress.courseId, id),
|
||||
});
|
||||
|
||||
const progressMap: Record<string, typeof progressRows[0]> = {};
|
||||
for (const p of progressRows) progressMap[p.topicId] = p;
|
||||
|
||||
const topicIds = topicRows.map((t) => t.id);
|
||||
|
||||
const lessonRows = topicIds.length
|
||||
? await db.query.lessons.findMany({ where: inArray(lessons.topicId, topicIds) })
|
||||
: [];
|
||||
|
||||
const lessonTopicIds = new Set(lessonRows.map((l) => l.topicId));
|
||||
|
||||
return {
|
||||
...course,
|
||||
topics: topicRows.map((t) => ({
|
||||
...t,
|
||||
prerequisiteTopicIds: JSON.parse(t.prerequisiteTopicIds ?? "[]"),
|
||||
progress: progressMap[t.id] ?? null,
|
||||
hasLesson: lessonTopicIds.has(t.id),
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { db } from "../../../db/index";
|
||||
import { courses } from "../../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id")!;
|
||||
const body = await readBody(event);
|
||||
|
||||
const course = await db.query.courses.findFirst({ where: eq(courses.id, id) });
|
||||
if (!course) throw createError({ statusCode: 404, message: "Course not found" });
|
||||
|
||||
const updates: Partial<typeof courses.$inferInsert> = {};
|
||||
|
||||
if (body.title !== undefined) {
|
||||
if (!body.title.trim()) throw createError({ statusCode: 400, message: "Title cannot be empty" });
|
||||
updates.title = body.title.trim();
|
||||
}
|
||||
if (body.subject !== undefined) updates.subject = body.subject;
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
throw createError({ statusCode: 400, message: "Nothing to update" });
|
||||
}
|
||||
|
||||
await db.update(courses).set(updates).where(eq(courses.id, id));
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { db } from "../../../db/index";
|
||||
import { uploads, courses } from "../../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { randomUUID } from "crypto";
|
||||
import { writeFile, mkdir } from "fs/promises";
|
||||
import { resolve } from "path";
|
||||
import { parsePdf } from "../../../utils/parsePdf";
|
||||
import { detectUploadType } from "../../../utils/detectUploadType";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id")!;
|
||||
|
||||
const course = await db.query.courses.findFirst({ where: eq(courses.id, id) });
|
||||
if (!course) throw createError({ statusCode: 404, message: "Course not found" });
|
||||
|
||||
const formData = await readFormData(event);
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!file) {
|
||||
throw createError({ statusCode: 400, message: "file is required" });
|
||||
}
|
||||
|
||||
const uploadDir = resolve(process.cwd(), "uploads", id);
|
||||
await mkdir(uploadDir, { recursive: true });
|
||||
|
||||
const uploadId = randomUUID();
|
||||
const safeFilename = `${uploadId}-${file.name.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
||||
const storedPath = resolve(uploadDir, safeFilename);
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
await writeFile(storedPath, buffer);
|
||||
|
||||
let extractedText: string | null = null;
|
||||
try {
|
||||
extractedText = await parsePdf(buffer);
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
|
||||
const detectedType = await detectUploadType(file.name, extractedText ?? "");
|
||||
|
||||
await db.insert(uploads).values({
|
||||
id: uploadId,
|
||||
courseId: id,
|
||||
filename: file.name,
|
||||
type: detectedType,
|
||||
storedPath,
|
||||
extractedText,
|
||||
});
|
||||
|
||||
return { uploadId, filename: file.name, type: detectedType };
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { db } from "../../db/index";
|
||||
import { courses, topics, userProgress } from "../../db/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const allCourses = await db.query.courses.findMany({
|
||||
orderBy: (c, { desc }) => desc(c.createdAt),
|
||||
});
|
||||
|
||||
const result = [];
|
||||
|
||||
for (const course of allCourses) {
|
||||
const topicRows = await db.query.topics.findMany({
|
||||
where: eq(topics.courseId, course.id),
|
||||
});
|
||||
|
||||
const topicCount = topicRows.length;
|
||||
|
||||
let completedCount = 0;
|
||||
if (topicCount > 0) {
|
||||
const progressRows = await db.query.userProgress.findMany({
|
||||
where: eq(userProgress.courseId, course.id),
|
||||
});
|
||||
completedCount = progressRows.filter((p) => p.lessonComplete).length;
|
||||
}
|
||||
|
||||
result.push({
|
||||
...course,
|
||||
topicCount,
|
||||
completedCount,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { db } from "../../db/index";
|
||||
import { courses } from "../../db/schema";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const id = randomUUID();
|
||||
|
||||
await db.insert(courses).values({
|
||||
id,
|
||||
title: "Untitled Course",
|
||||
subject: "Unknown",
|
||||
status: "processing",
|
||||
});
|
||||
|
||||
return { courseId: id };
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { db } from "../../../db/index";
|
||||
import { topics, lessons } from "../../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
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 lesson = await db.query.lessons.findFirst({ where: eq(lessons.topicId, id) });
|
||||
if (!lesson) throw createError({ statusCode: 404, message: "Lesson not yet generated" });
|
||||
|
||||
return {
|
||||
...lesson,
|
||||
content: JSON.parse(lesson.content),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { db } from "../../../db/index";
|
||||
import { topics, userProgress } from "../../../db/schema";
|
||||
import { eq, and } 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 ?? {};
|
||||
|
||||
const existing = await db.query.userProgress.findFirst({
|
||||
where: and(
|
||||
eq(userProgress.topicId, id),
|
||||
eq(userProgress.courseId, topic.courseId)
|
||||
),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await db
|
||||
.update(userProgress)
|
||||
.set({
|
||||
lessonComplete: lessonComplete ?? existing.lessonComplete,
|
||||
quizScore: quizScore ?? existing.quizScore,
|
||||
tookBranches: tookBranches ?? existing.tookBranches,
|
||||
branchCount: branchCount ?? existing.branchCount,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(userProgress.id, existing.id));
|
||||
} else {
|
||||
await db.insert(userProgress).values({
|
||||
id: randomUUID(),
|
||||
courseId: topic.courseId,
|
||||
topicId: id,
|
||||
lessonComplete: lessonComplete ?? false,
|
||||
quizScore: quizScore ?? null,
|
||||
tookBranches: tookBranches ?? false,
|
||||
branchCount: branchCount ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { db } from "../../../db/index";
|
||||
import { topics, quizQuestions } from "../../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
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 questions = await db.query.quizQuestions.findMany({
|
||||
where: eq(quizQuestions.topicId, id),
|
||||
});
|
||||
|
||||
if (questions.length === 0) throw createError({ statusCode: 404, message: "Quiz not yet generated" });
|
||||
|
||||
return questions.map((q) => ({
|
||||
...q,
|
||||
options: q.options ? JSON.parse(q.options) : null,
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { db } from "../../db/index";
|
||||
import { uploads } from "../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id")!;
|
||||
|
||||
const upload = await db.query.uploads.findFirst({ where: eq(uploads.id, id) });
|
||||
if (!upload) throw createError({ statusCode: 404, message: "Upload not found" });
|
||||
|
||||
const body = await readBody(event);
|
||||
const { type } = body ?? {};
|
||||
|
||||
const valid = ["slides", "past_paper", "lab_worksheet"];
|
||||
if (!type || !valid.includes(type)) {
|
||||
throw createError({ statusCode: 400, message: "type must be slides, past_paper, or lab_worksheet" });
|
||||
}
|
||||
|
||||
await db
|
||||
.update(uploads)
|
||||
.set({ type: type as "slides" | "past_paper" | "lab_worksheet" })
|
||||
.where(eq(uploads.id, id));
|
||||
|
||||
return { ok: true, type };
|
||||
});
|
||||
Reference in New Issue
Block a user