import { eq, inArray, isNull, sql } from 'drizzle-orm'; import type { Db } from '../db/client.js'; import { cards, lessons } from '../db/schema.js'; import { ApiError } from '../lib/errors.js'; import type { Lesson, LessonTreeNode, LessonCreateInput, LessonUpdateInput, LessonMoveInput, } from '@flashcard/shared'; function rowToLesson(r: typeof lessons.$inferSelect): Lesson { return { id: r.id, parentId: r.parentId ?? null, name: r.name, description: r.description ?? null, position: r.position, bidirectional: r.bidirectional, ownerId: r.ownerId ?? null, visibility: r.visibility, isCurated: r.isCurated, sourceLessonId: r.sourceLessonId ?? null, createdAt: r.createdAt, updatedAt: r.updatedAt, }; } async function nextPosition(db: Db, parentId: number | null): Promise { const rows = parentId == null ? db.select({ pos: lessons.position }).from(lessons).where(isNull(lessons.parentId)).all() : db.select({ pos: lessons.position }).from(lessons).where(eq(lessons.parentId, parentId)).all(); return rows.length === 0 ? 0 : Math.max(...rows.map((r) => r.pos)) + 1; } export async function createLesson(db: Db, input: LessonCreateInput): Promise { const parentId = input.parentId ?? null; if (parentId !== null) { const exists = db.select().from(lessons).where(eq(lessons.id, parentId)).get(); if (!exists) throw ApiError.notFound('Parent lesson'); } const position = await nextPosition(db, parentId); const [row] = db.insert(lessons).values({ name: input.name, parentId, description: input.description ?? null, bidirectional: input.bidirectional ?? false, position, }).returning().all(); return rowToLesson(row!); } export async function updateLesson(db: Db, id: number, input: LessonUpdateInput): Promise { const existing = db.select().from(lessons).where(eq(lessons.id, id)).get(); if (!existing) throw ApiError.notFound('Lesson'); const [row] = db.update(lessons).set({ ...(input.name !== undefined && { name: input.name }), ...(input.description !== undefined && { description: input.description }), ...(input.bidirectional !== undefined && { bidirectional: input.bidirectional }), updatedAt: Math.floor(Date.now() / 1000), }).where(eq(lessons.id, id)).returning().all(); return rowToLesson(row!); } export async function deleteLesson(db: Db, id: number): Promise { const existing = db.select().from(lessons).where(eq(lessons.id, id)).get(); if (!existing) throw ApiError.notFound('Lesson'); const ids = await getDescendantLessonIds(db, id); db.delete(lessons).where(inArray(lessons.id, ids)).run(); } export async function moveLesson(db: Db, id: number, input: LessonMoveInput): Promise { const existing = db.select().from(lessons).where(eq(lessons.id, id)).get(); if (!existing) throw ApiError.notFound('Lesson'); if (input.parentId !== null) { const p = db.select().from(lessons).where(eq(lessons.id, input.parentId)).get(); if (!p) throw ApiError.notFound('Parent lesson'); let cursor: number | null = input.parentId; while (cursor !== null) { if (cursor === id) throw ApiError.validation('Cannot move lesson into its own descendant'); const row = db.select({ parentId: lessons.parentId }).from(lessons).where(eq(lessons.id, cursor)).get(); cursor = row?.parentId ?? null; } } const [row] = db.update(lessons).set({ parentId: input.parentId, position: input.position, updatedAt: Math.floor(Date.now() / 1000), }).where(eq(lessons.id, id)).returning().all(); return rowToLesson(row!); } export async function getLessonTree(db: Db): Promise { const all = db.select().from(lessons).orderBy(lessons.position).all(); const counts = db .select({ lessonId: cards.lessonId, count: sql`count(*)`.as('count') }) .from(cards) .groupBy(cards.lessonId) .all(); const countMap = new Map(counts.map((c) => [c.lessonId, Number(c.count)])); const nodes = new Map(); for (const r of all) { nodes.set(r.id, { ...rowToLesson(r), children: [], cardCount: countMap.get(r.id) ?? 0 }); } const roots: LessonTreeNode[] = []; for (const n of nodes.values()) { if (n.parentId === null) roots.push(n); else nodes.get(n.parentId)?.children.push(n); } return roots; } export async function getDescendantLessonIds(db: Db, rootId: number): Promise { const all = db.select({ id: lessons.id, parentId: lessons.parentId }).from(lessons).all(); const byParent = new Map(); for (const r of all) { const key = r.parentId ?? null; if (!byParent.has(key)) byParent.set(key, []); byParent.get(key)!.push(r.id); } const result: number[] = [rootId]; const stack = [rootId]; while (stack.length) { const cur = stack.pop()!; for (const child of byParent.get(cur) ?? []) { result.push(child); stack.push(child); } } return result; }