import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { auth } from '@/auth' import { parseNote } from '@/lib/utils' import { createNoteHistorySnapshot, getNoteHistoryMode, isNoteHistoryEnabledForUser, shouldCaptureHistorySnapshot, shouldCreateAutoSnapshot, } from '@/lib/note-history' // GET /api/notes/[id] - Get a single note export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const session = await auth() if (!session?.user?.id) { return NextResponse.json( { success: false, error: 'Unauthorized' }, { status: 401 } ) } try { const { id } = await params const note = await prisma.note.findUnique({ where: { id } }) if (!note) { return NextResponse.json( { success: false, error: 'Note not found' }, { status: 404 } ) } if (note.userId !== session.user.id) { const share = await prisma.noteShare.findUnique({ where: { noteId_userId: { noteId: note.id, userId: session.user.id } } }) if (!share || share.status !== 'accepted') { return NextResponse.json( { success: false, error: 'Forbidden' }, { status: 403 } ) } } return NextResponse.json({ success: true, data: parseNote(note) }) } catch (error) { console.error('Error fetching note:', error) return NextResponse.json( { success: false, error: 'Failed to fetch note' }, { status: 500 } ) } } // PUT /api/notes/[id] - Update a note export async function PUT( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const session = await auth() if (!session?.user?.id) { return NextResponse.json( { success: false, error: 'Unauthorized' }, { status: 401 } ) } try { const { id } = await params const existingNote = await prisma.note.findUnique({ where: { id } }) if (!existingNote) { return NextResponse.json( { success: false, error: 'Note not found' }, { status: 404 } ) } if (existingNote.userId !== session.user.id) { return NextResponse.json( { success: false, error: 'Forbidden' }, { status: 403 } ) } const body = await request.json() // Whitelist allowed fields to prevent mass assignment const allowedFields = ['title', 'content', 'color', 'isPinned', 'isArchived', 'type', 'isMarkdown', 'size', 'notebookId'] const updateData: Record = {} for (const key of allowedFields) { if (key in body) { updateData[key] = body[key] } } if ('checkItems' in body) { updateData.checkItems = body.checkItems ?? null } if ('labels' in body) { updateData.labels = body.labels ?? null } // Only update if data actually changed const hasChanges = Object.keys(updateData).some((key) => { const newValue = updateData[key] const oldValue = (existingNote as any)[key] // Handle arrays/objects by comparing JSON if (typeof newValue === 'object' && newValue !== null) { return JSON.stringify(newValue) !== JSON.stringify(oldValue) } return newValue !== oldValue }) // If no changes, return existing note without updating timestamp if (!hasChanges) { return NextResponse.json({ success: true, data: parseNote(existingNote), }) } const note = await prisma.note.update({ where: { id }, data: updateData, }) try { const historyEnabled = existingNote.historyEnabled === true if (historyEnabled && shouldCaptureHistorySnapshot(updateData)) { const mode = await getNoteHistoryMode(session.user.id) if (mode === 'auto') { const shouldAuto = await shouldCreateAutoSnapshot({ noteId: id, userId: session.user.id, updateData, existingContent: existingNote.content ?? '', existingTitle: existingNote.title ?? null, }) if (shouldAuto) { await createNoteHistorySnapshot({ noteId: id, userId: session.user.id, reason: 'api:update', }) } } // manual mode: no auto-snapshot } } catch (snapshotError) { console.error('[HISTORY] Failed to create snapshot from /api/notes/[id] PUT:', snapshotError) } // Fire-and-forget: sync [[wikilinks]] in background after content change if ('content' in updateData) { syncNoteLinksBackground(id, session.user.id, note.content).catch(() => {}) } return NextResponse.json({ success: true, data: parseNote(note) }) } catch (error) { console.error('Error updating note:', error) return NextResponse.json( { success: false, error: 'Failed to update note' }, { status: 500 } ) } } /** Background job: parse [[wikilinks]] and sync NoteLink table */ async function syncNoteLinksBackground(noteId: string, userId: string, content: string) { const WIKILINK_RE = /\[\[([^\]|#]+?)(?:[|#][^\]]+)?\]\]/g const plain = content.replace(/<[^>]+>/g, ' ') const wikilinks: { title: string; snippet: string }[] = [] const seen = new Set() let match: RegExpExecArray | null WIKILINK_RE.lastIndex = 0 while ((match = WIKILINK_RE.exec(plain)) !== null) { const title = match[1].trim() if (!title || seen.has(title.toLowerCase())) continue seen.add(title.toLowerCase()) const start = Math.max(0, match.index - 50) const end = Math.min(plain.length, match.index + match[0].length + 50) const snippet = plain.slice(start, end).replace(/\s+/g, ' ').trim() wikilinks.push({ title, snippet }) } const upsertedIds: string[] = [] for (const { title, snippet } of wikilinks) { let targetNote = await prisma.note.findFirst({ where: { userId, title: { equals: title, mode: 'insensitive' }, trashedAt: null }, select: { id: true }, }) if (!targetNote) { targetNote = await prisma.note.create({ data: { title, content: '', userId, type: 'richtext', color: 'default', isMarkdown: true, order: 0 }, select: { id: true }, }) } if (targetNote.id === noteId) continue await (prisma as any).noteLink.upsert({ where: { sourceNoteId_targetNoteId: { sourceNoteId: noteId, targetNoteId: targetNote.id } }, update: { contextSnippet: snippet.slice(0, 200) }, create: { id: crypto.randomUUID(), sourceNoteId: noteId, targetNoteId: targetNote.id, contextSnippet: snippet.slice(0, 200) }, }) upsertedIds.push(targetNote.id) } if (upsertedIds.length > 0) { await (prisma as any).noteLink.deleteMany({ where: { sourceNoteId: noteId, targetNoteId: { notIn: upsertedIds } }, }) } else { await (prisma as any).noteLink.deleteMany({ where: { sourceNoteId: noteId } }) } } // DELETE /api/notes/[id] - Delete a note export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const session = await auth() if (!session?.user?.id) { return NextResponse.json( { success: false, error: 'Unauthorized' }, { status: 401 } ) } try { const { id } = await params const existingNote = await prisma.note.findUnique({ where: { id } }) if (!existingNote) { return NextResponse.json( { success: false, error: 'Note not found' }, { status: 404 } ) } if (existingNote.userId !== session.user.id) { return NextResponse.json( { success: false, error: 'Forbidden' }, { status: 403 } ) } await prisma.note.update({ where: { id }, data: { trashedAt: new Date() } }) return NextResponse.json({ success: true, message: 'Note moved to trash' }) } catch (error) { console.error('Error deleting note:', error) return NextResponse.json( { success: false, error: 'Failed to delete note' }, { status: 500 } ) } }