fix: sync admin UI schema with plugin (UUID keys, lesson_media table)

Schema now matches plugin Data/LessonStore.cs exactly:
- id TEXT PRIMARY KEY (UUIDs)
- lesson_media table (not media_items)
- Full column set with NOT NULL constraints matching plugin

Addresses PR #14 review feedback.
This commit is contained in:
engineering Agent 2026-05-15 21:50:21 -07:00
parent 6e7d2372e7
commit e4967e1cda

View file

@ -16,26 +16,35 @@ fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
const db = new Database(DB_PATH);
// Initialize schema if needed
// UUID helper
function generateId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
// Initialize schema — must match plugin Data/LessonStore.cs
const now = () => new Date().toISOString();
db.exec(`
CREATE TABLE IF NOT EXISTS lessons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
grade INTEGER,
subject TEXT,
lesson_date TEXT,
grade INTEGER NOT NULL,
subject TEXT NOT NULL,
lesson_date TEXT NOT NULL,
description TEXT,
source_path TEXT,
source_path TEXT NOT NULL,
compiled_html TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS media_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
lesson_id INTEGER NOT NULL,
CREATE TABLE IF NOT EXISTS lesson_media (
lesson_id TEXT NOT NULL,
jellyfin_item_id TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (lesson_id, jellyfin_item_id),
FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE
);
`);
@ -68,7 +77,7 @@ app.get('/api/lessons/:id', (req, res) => {
try {
const lesson = db.prepare('SELECT * FROM lessons WHERE id = ?').get(req.params.id);
if (!lesson) return res.status(404).json({ error: 'Lesson not found' });
const mediaItems = db.prepare('SELECT * FROM media_items WHERE lesson_id = ? ORDER BY sort_order').all(req.params.id);
const mediaItems = db.prepare('SELECT * FROM lesson_media WHERE lesson_id = ? ORDER BY sort_order').all(req.params.id);
res.json({ ...lesson, mediaItems });
} catch (err) {
res.status(500).json({ error: err.message });
@ -83,31 +92,30 @@ app.post('/api/lessons', (req, res) => {
if (!title) return res.status(400).json({ error: 'Title is required' });
const upsertLesson = db.transaction(() => {
let lessonId = id;
if (lessonId) {
// Update existing
db.prepare(`
UPDATE lessons
SET title = ?, grade = ?, subject = ?, lesson_date = ?, description = ?,
source_path = ?, compiled_html = ?, updated_at = datetime('now')
WHERE id = ?
`).run(title, grade || null, subject || null, lessonDate || null, description || null,
sourcePath || null, compiledHtml || null, lessonId);
// Replace media items
db.prepare('DELETE FROM media_items WHERE lesson_id = ?').run(lessonId);
} else {
// Create new
const result = db.prepare(`
INSERT INTO lessons (title, grade, subject, lesson_date, description, source_path, compiled_html)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(title, grade || null, subject || null, lessonDate || null, description || null,
sourcePath || null, compiledHtml || null);
lessonId = result.lastInsertRowid;
}
let lessonId = id || generateId();
const ts = now();
const existing = id ? db.prepare('SELECT created_at FROM lessons WHERE id = ?').get(id) : null;
const createdAt = existing ? existing.created_at : ts;
// Insert media items
db.prepare(`
INSERT INTO lessons (id, title, grade, subject, lesson_date, description, source_path, compiled_html, created_at, updated_at)
VALUES (@id, @title, @grade, @subject, @lesson_date, @description, @source_path, @compiled_html, @created_at, @updated_at)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title, grade = excluded.grade, subject = excluded.subject,
lesson_date = excluded.lesson_date, description = excluded.description,
source_path = excluded.source_path, compiled_html = excluded.compiled_html,
updated_at = excluded.updated_at
`).run({
id: lessonId, title, grade: grade || 0,
subject: subject || '', lesson_date: lessonDate || new Date().toISOString().split('T')[0],
description: description || null, source_path: sourcePath || '',
compiled_html: compiledHtml || null, created_at: createdAt, updated_at: ts
});
// Replace media items
db.prepare('DELETE FROM lesson_media WHERE lesson_id = ?').run(lessonId);
if (mediaItems && Array.isArray(mediaItems)) {
const insertMedia = db.prepare('INSERT INTO media_items (lesson_id, jellyfin_item_id, sort_order) VALUES (?, ?, ?)');
const insertMedia = db.prepare('INSERT INTO lesson_media (lesson_id, jellyfin_item_id, sort_order) VALUES (?, ?, ?)');
for (const item of mediaItems) {
insertMedia.run(lessonId, item.jellyfinItemId, item.sortOrder || 0);
}
@ -118,7 +126,7 @@ app.post('/api/lessons', (req, res) => {
const lessonId = upsertLesson();
const lesson = db.prepare('SELECT * FROM lessons WHERE id = ?').get(lessonId);
const media = db.prepare('SELECT * FROM media_items WHERE lesson_id = ? ORDER BY sort_order').all(lessonId);
const media = db.prepare('SELECT * FROM lesson_media WHERE lesson_id = ? ORDER BY sort_order').all(lessonId);
res.json({ ...lesson, mediaItems: media });
} catch (err) {
res.status(500).json({ error: err.message });