initialize project with basic structure and dependencies

This commit is contained in:
ImBenji
2026-04-27 20:56:49 +01:00
parent 59b85afd10
commit 8548717074
85 changed files with 19634 additions and 0 deletions
+20
View File
@@ -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" };
});
+43
View File
@@ -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),
})),
};
});
+27
View File
@@ -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 };
});
+52
View File
@@ -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 };
});
+35
View File
@@ -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;
});
+16
View File
@@ -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 };
});