harden database interactions and improve error handling
This commit is contained in:
@@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
|
||||
const pathStr = Array.isArray(pathParam) ? pathParam.join("/") : pathParam;
|
||||
|
||||
// prevent path traversal
|
||||
const baseDir = resolve(process.cwd(), "private/audio");
|
||||
const baseDir = resolve(process.cwd(), "data/audio");
|
||||
const filePath = normalize(resolve(baseDir, pathStr));
|
||||
|
||||
if (!filePath.startsWith(baseDir)) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from "../../../db/index";
|
||||
import { courses, topics, userProgress, lessons } from "../../../db/schema";
|
||||
import { courses, topics, lessons } from "../../../db/schema";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -16,13 +16,6 @@ export default defineEventHandler(async (event) => {
|
||||
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
|
||||
@@ -37,7 +30,6 @@ export default defineEventHandler(async (event) => {
|
||||
topics: topicRows.map((t) => ({
|
||||
...t,
|
||||
prerequisiteTopicIds: JSON.parse(t.prerequisiteTopicIds ?? "[]"),
|
||||
progress: progressMap[t.id] ?? null,
|
||||
hasLesson: !!lessonMap[t.id],
|
||||
lessonCost: lessonMap[t.id]?.costTotal ?? null,
|
||||
})),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "../../db/index";
|
||||
import { courses, topics, userProgress } from "../../db/schema";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { courses, topics } from "../../db/schema";
|
||||
import { inArray } from "drizzle-orm";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const allCourses = await db.query.courses.findMany({
|
||||
@@ -11,27 +11,18 @@ export default defineEventHandler(async () => {
|
||||
|
||||
const courseIds = allCourses.map((c) => c.id);
|
||||
|
||||
const [allTopics, allProgress] = await Promise.all([
|
||||
db.query.topics.findMany({ where: inArray(topics.courseId, courseIds) }),
|
||||
db.query.userProgress.findMany({ where: inArray(userProgress.courseId, courseIds) }),
|
||||
]);
|
||||
const allTopics = await db.query.topics.findMany({ where: inArray(topics.courseId, courseIds) });
|
||||
|
||||
// group in memory
|
||||
const topicsByCourse = new Map<string, number>();
|
||||
const topicsByCourse = new Map<string, string[]>();
|
||||
for (const t of allTopics) {
|
||||
topicsByCourse.set(t.courseId, (topicsByCourse.get(t.courseId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const completedByCourse = new Map<string, number>();
|
||||
for (const p of allProgress) {
|
||||
if (p.lessonComplete) {
|
||||
completedByCourse.set(p.courseId, (completedByCourse.get(p.courseId) ?? 0) + 1);
|
||||
}
|
||||
const arr = topicsByCourse.get(t.courseId) ?? [];
|
||||
arr.push(t.id);
|
||||
topicsByCourse.set(t.courseId, arr);
|
||||
}
|
||||
|
||||
return allCourses.map((course) => ({
|
||||
...course,
|
||||
topicCount: topicsByCourse.get(course.id) ?? 0,
|
||||
completedCount: completedByCourse.get(course.id) ?? 0,
|
||||
topicCount: topicsByCourse.get(course.id)?.length ?? 0,
|
||||
topicIds: topicsByCourse.get(course.id) ?? [],
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { db } from "../../../../db/index";
|
||||
import { lessons } from "../../../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
generateStepTTS,
|
||||
generateQuestionTTS,
|
||||
generateOptionTTS,
|
||||
} from "../../../../utils/generateTTS";
|
||||
import { verifyLicenseKey } from "../../../../utils/license";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const key = getHeader(event, "x-license-key") ?? "";
|
||||
if (!verifyLicenseKey(key)) {
|
||||
throw createError({ statusCode: 401, message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const topicId = getRouterParam(event, "id")!;
|
||||
|
||||
const lesson = await db.query.lessons.findFirst({ where: eq(lessons.topicId, topicId) });
|
||||
if (!lesson) throw createError({ statusCode: 404, message: "Lesson not found" });
|
||||
|
||||
const content = JSON.parse(lesson.content);
|
||||
const steps: any[] = content.steps ?? [];
|
||||
|
||||
type Task = () => Promise<void>;
|
||||
const tasks: Task[] = [];
|
||||
|
||||
for (let si = 0; si < steps.length; si++) {
|
||||
const step = steps[si];
|
||||
const siCopy = si;
|
||||
|
||||
if (step.type === "concept" || step.type === "example") {
|
||||
const text = [step.body, step.callout].filter(Boolean).join(" ");
|
||||
if (!text.trim()) continue;
|
||||
|
||||
tasks.push(async () => {
|
||||
const r = await generateStepTTS(text, lesson.id, siCopy);
|
||||
if (r) { step.audioPath = r.audioPath; step.audioChunks = r.audioChunks; }
|
||||
});
|
||||
|
||||
} else if (step.type === "summary") {
|
||||
const text = Array.isArray(step.bullets) ? step.bullets.join(". ") : "";
|
||||
if (!text.trim()) continue;
|
||||
|
||||
tasks.push(async () => {
|
||||
const r = await generateStepTTS(text, lesson.id, siCopy);
|
||||
if (r) { step.audioPath = r.audioPath; step.audioChunks = r.audioChunks; }
|
||||
});
|
||||
|
||||
} else if (step.type === "question") {
|
||||
if (step.body?.trim()) {
|
||||
tasks.push(async () => {
|
||||
const r = await generateQuestionTTS(step.body, lesson.id, siCopy);
|
||||
if (r) { step.questionAudioPath = r.audioPath; step.questionAudioChunks = r.audioChunks; }
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(step.options)) {
|
||||
step.optionAudioPaths = new Array(step.options.length).fill(null);
|
||||
for (let oi = 0; oi < step.options.length; oi++) {
|
||||
const optText = step.options[oi];
|
||||
const oiCopy = oi;
|
||||
if (optText?.trim()) {
|
||||
tasks.push(async () => {
|
||||
const r = await generateOptionTTS(optText, lesson.id, siCopy, oiCopy);
|
||||
if (r) step.optionAudioPaths[oiCopy] = r.audioPath;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BATCH = 3;
|
||||
for (let i = 0; i < tasks.length; i += BATCH) {
|
||||
await Promise.all(tasks.slice(i, i + BATCH).map((fn) => fn()));
|
||||
}
|
||||
|
||||
content.steps = steps;
|
||||
await db.update(lessons).set({ content: JSON.stringify(content) }).where(eq(lessons.id, lesson.id));
|
||||
|
||||
return { status: "ready" };
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { db } from "../../../../db/index";
|
||||
import { topics, lessons } from "../../../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { generateLesson } from "../../../../utils/generateLesson";
|
||||
import { verifyLicenseKey } from "../../../../utils/license";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const key = getHeader(event, "x-license-key") ?? "";
|
||||
if (!verifyLicenseKey(key)) {
|
||||
throw createError({ statusCode: 401, message: "Unauthorized" });
|
||||
}
|
||||
|
||||
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" });
|
||||
|
||||
// wipe existing lesson
|
||||
await db.delete(lessons).where(eq(lessons.topicId, id));
|
||||
|
||||
// reset topic status so generateLesson doesnt bail
|
||||
await db.update(topics).set({ status: "pending" }).where(eq(topics.id, id));
|
||||
|
||||
try {
|
||||
await generateLesson(id);
|
||||
return { status: "ready" };
|
||||
} catch (err: any) {
|
||||
console.error(`[dev/regenerate-lesson] topic ${id}: ${err?.message ?? err}`);
|
||||
return { status: "error" };
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user