import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { auth } from '@/auth' const WIKILINK_RE = /\[\[([^\]|#]+?)(?:[|#][^\]]+)?\]\]/g /** * Extract [[wikilink]] targets from markdown/html content. * Returns deduplicated list of linked note titles. */ function extractWikilinks(content: string): { title: string; snippet: string }[] { // Strip HTML tags const plain = content.replace(/<[^>]+>/g, ' ') const results: { 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()) // Extract snippet: 50 chars before + after the match 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() results.push({ title, snippet }) } return results } // POST /api/notes/[id]/sync-links // Parse [[wikilinks]] in note content and sync the NoteLink table. // Called automatically after note save. export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const { id } = await params const userId = session.user.id const note = await prisma.note.findUnique({ where: { id }, select: { id: true, userId: true, content: true }, }) if (!note || note.userId !== userId) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) } const wikilinks = extractWikilinks(note.content) // For each wikilink, find or create the target note const upsertedLinks: string[] = [] for (const { title, snippet } of wikilinks) { // Find target note by title (case-insensitive) belonging to same user let targetNote = await prisma.note.findFirst({ where: { userId, title: { equals: title, mode: 'insensitive' }, trashedAt: null, }, select: { id: true }, }) if (!targetNote) { // Create a stub note so the link resolves targetNote = await prisma.note.create({ data: { title, content: '', userId, type: 'richtext', color: 'default', isMarkdown: true, order: 0, }, select: { id: true }, }) } // Skip self-links if (targetNote.id === id) continue // Upsert the NoteLink await (prisma as any).noteLink.upsert({ where: { sourceNoteId_targetNoteId: { sourceNoteId: id, targetNoteId: targetNote.id, }, }, update: { contextSnippet: snippet.slice(0, 200) }, create: { id: crypto.randomUUID(), sourceNoteId: id, targetNoteId: targetNote.id, contextSnippet: snippet.slice(0, 200), }, }) upsertedLinks.push(targetNote.id) } // Delete obsolete links (links that existed before but wikilink was removed) if (upsertedLinks.length > 0) { await (prisma as any).noteLink.deleteMany({ where: { sourceNoteId: id, targetNoteId: { notIn: upsertedLinks }, }, }) } else { // No wikilinks at all — remove all outgoing links from this note await (prisma as any).noteLink.deleteMany({ where: { sourceNoteId: id }, }) } return NextResponse.json({ synced: upsertedLinks.length }) }