Files
Learn_System/backend/src/controllers/bookmarkController.js
T
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00

104 lines
4.3 KiB
JavaScript

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 };