const db = require('../db/db'); const VALID_TYPES = ['lesson', 'course', 'file', 'question']; /* ── GET /api/bookmarks?type=lesson ── list user bookmarks ─────────── */ function list(req, res) { const uid = req.user.id; const { type } = req.query; let where = 'WHERE b.user_id = ?'; const args = [uid]; if (type && VALID_TYPES.includes(type)) { where += ' AND b.entity_type = ?'; args.push(type); } const rows = db.prepare(` SELECT b.id, b.entity_type, b.entity_id, b.created_at FROM bookmarks b ${where} ORDER BY b.created_at DESC `).all(...args); // Batch-fetch titles by type to avoid N+1 const byType = {}; for (const r of rows) (byType[r.entity_type] ||= []).push(r); const titleMap = new Map(); if (byType.lesson?.length) { const ids = byType.lesson.map(r => r.entity_id); const ph = ids.map(() => '?').join(','); for (const l of db.prepare(`SELECT l.id, l.title, c.title AS course_title, c.id AS course_id FROM lessons l JOIN courses c ON l.course_id = c.id WHERE l.id IN (${ph})`).all(...ids)) titleMap.set(`lesson:${l.id}`, { title: l.title, courseTitle: l.course_title, courseId: l.course_id }); } if (byType.course?.length) { const ids = byType.course.map(r => r.entity_id); const ph = ids.map(() => '?').join(','); for (const c of db.prepare(`SELECT id, title, subject_slug, cover_emoji FROM courses WHERE id IN (${ph})`).all(...ids)) titleMap.set(`course:${c.id}`, { title: c.title, subjectSlug: c.subject_slug, coverEmoji: c.cover_emoji }); } if (byType.file?.length) { const ids = byType.file.map(r => r.entity_id); const ph = ids.map(() => '?').join(','); for (const f of db.prepare(`SELECT id, title, original_name FROM files WHERE id IN (${ph})`).all(...ids)) titleMap.set(`file:${f.id}`, { title: f.title || f.original_name }); } if (byType.question?.length) { const ids = byType.question.map(r => r.entity_id); const ph = ids.map(() => '?').join(','); for (const q of db.prepare(`SELECT id, text FROM questions WHERE id IN (${ph})`).all(...ids)) titleMap.set(`question:${q.id}`, { title: q.text.slice(0, 100) }); } const enriched = rows.map(r => { const info = titleMap.get(`${r.entity_type}:${r.entity_id}`); if (!info) return null; const { title, ...extra } = info; return { ...r, title, ...extra }; }).filter(Boolean); res.json(enriched); } /* ── POST /api/bookmarks ── add bookmark ───────────────────────────── */ function add(req, res) { const { entityType, entityId } = req.body; if (!entityType || !entityId) return res.status(400).json({ error: 'entityType and entityId required' }); if (!VALID_TYPES.includes(entityType)) return res.status(400).json({ error: 'Invalid entity type' }); try { const r = db.prepare( 'INSERT INTO bookmarks (user_id, entity_type, entity_id) VALUES (?, ?, ?)' ).run(req.user.id, entityType, entityId); res.status(201).json({ id: Number(r.lastInsertRowid) }); } catch (e) { if (e.message.includes('UNIQUE')) return res.status(409).json({ error: 'Already bookmarked' }); throw e; } } /* ── DELETE /api/bookmarks/:id ── remove bookmark ──────────────────── */ function remove(req, res) { const bm = db.prepare('SELECT id FROM bookmarks WHERE id = ? AND user_id = ?').get(req.params.id, req.user.id); if (!bm) return res.status(404).json({ error: 'Bookmark not found' }); db.prepare('DELETE FROM bookmarks WHERE id = ?').run(bm.id); res.json({ ok: true }); } /* ── DELETE /api/bookmarks/entity/:type/:entityId ── remove by entity ── */ function removeByEntity(req, res) { const { type, entityId } = req.params; db.prepare('DELETE FROM bookmarks WHERE user_id = ? AND entity_type = ? AND entity_id = ?').run(req.user.id, type, entityId); res.json({ ok: true }); } /* ── GET /api/bookmarks/check/:type/:entityId ── check if bookmarked ── */ function check(req, res) { const { type, entityId } = req.params; const row = db.prepare('SELECT id FROM bookmarks WHERE user_id = ? AND entity_type = ? AND entity_id = ?').get(req.user.id, type, entityId); res.json({ bookmarked: !!row, id: row?.id || null }); } module.exports = { list, add, remove, removeByEntity, check };