import { randomUUID } from 'crypto' import type { OutlineItem, OutlineStatus } from '../../../shared/types' import type { SqliteDb } from '../types' function mapRow(row: Record): OutlineItem { return { id: row.id as string, parentId: (row.parent_id as string | null) ?? null, title: row.title as string, description: (row.description as string) ?? '', status: row.status as OutlineStatus, expectedWordCount: (row.expected_word_count as number | null) ?? null, chapterId: (row.chapter_id as string | null) ?? null, sortOrder: row.sort_order as number, createdAt: row.created_at as string, updatedAt: row.updated_at as string } } function buildTree(items: OutlineItem[]): OutlineItem[] { const byParent = new Map() for (const item of items) { const key = item.parentId if (!byParent.has(key)) byParent.set(key, []) byParent.get(key)!.push({ ...item, children: [] }) } const attach = (parentId: string | null): OutlineItem[] => { const nodes = byParent.get(parentId) ?? [] nodes.sort((a, b) => a.sortOrder - b.sortOrder) for (const node of nodes) { node.children = attach(node.id) } return nodes } return attach(null) } export class OutlineRepository { constructor(private db: SqliteDb) {} create(parentId: string | null, title: string, sortOrder: number): OutlineItem { const id = randomUUID() this.db .prepare( `INSERT INTO outline_items (id, parent_id, title, sort_order) VALUES (?, ?, ?, ?)` ) .run(id, parentId, title, sortOrder) return this.get(id)! } get(id: string): OutlineItem | null { const row = this.db.prepare('SELECT * FROM outline_items WHERE id = ?').get(id) as | Record | undefined return row ? mapRow(row) : null } listFlat(): OutlineItem[] { return ( this.db.prepare('SELECT * FROM outline_items ORDER BY sort_order').all() as Record[] ).map(mapRow) } listTree(): OutlineItem[] { return buildTree(this.listFlat()) } update( id: string, patch: Partial<{ title: string description: string status: OutlineStatus expectedWordCount: number | null chapterId: string | null sortOrder: number }> ): OutlineItem { const existing = this.get(id) if (!existing) throw new Error('Outline item not found') this.db .prepare( `UPDATE outline_items SET title = ?, description = ?, status = ?, expected_word_count = ?, chapter_id = ?, sort_order = ?, updated_at = datetime('now') WHERE id = ?` ) .run( patch.title ?? existing.title, patch.description ?? existing.description, patch.status ?? existing.status, patch.expectedWordCount !== undefined ? patch.expectedWordCount : existing.expectedWordCount, patch.chapterId !== undefined ? patch.chapterId : existing.chapterId, patch.sortOrder ?? existing.sortOrder, id ) return this.get(id)! } delete(id: string): void { this.db.prepare('DELETE FROM outline_items WHERE id = ?').run(id) } move(id: string, parentId: string | null, sortOrder: number): OutlineItem { const existing = this.get(id) if (!existing) throw new Error('Outline item not found') this.db .prepare( `UPDATE outline_items SET parent_id = ?, sort_order = ?, updated_at = datetime('now') WHERE id = ?` ) .run(parentId, sortOrder, id) return this.get(id)! } listUncovered(): OutlineItem[] { return ( this.db .prepare('SELECT * FROM outline_items WHERE chapter_id IS NULL ORDER BY sort_order') .all() as Record[] ).map(mapRow) } nextSortOrder(parentId: string | null): number { const row = parentId ? (this.db .prepare('SELECT MAX(sort_order) AS maxOrder FROM outline_items WHERE parent_id = ?') .get(parentId) as { maxOrder: number | null }) : (this.db .prepare('SELECT MAX(sort_order) AS maxOrder FROM outline_items WHERE parent_id IS NULL') .get() as { maxOrder: number | null }) return (row?.maxOrder ?? -1) + 1 } }