feat: MVP-2 completion - search, quick add, backlinks, guided forms
## Search & Retrieval - Improved search ranking with scoring (title match, favorites, recency) - Highlight matches with excerpt extraction - Fuzzy search with string-similarity - Unified noteQuery function ## Quick Capture - Quick Add API (POST /api/notes/quick) with type prefixes - Quick add parser with tag extraction - Global Quick Add UI (Ctrl+N shortcut) - Tag autocomplete in forms ## Note Relations - Automatic backlinks with sync on create/update/delete - Backlinks API (GET /api/notes/[id]/backlinks) - Related notes with scoring and reasons ## Guided Forms - Type-specific form fields (command, snippet, decision, recipe, procedure, inventory) - Serialization to/from markdown - Tag suggestions based on content (GET /api/tags/suggest) ## UX by Type - Command: Copy button for code blocks - Snippet: Syntax highlighting with react-syntax-highlighter - Procedure: Interactive checkboxes ## Quality - Standardized error handling across all APIs - Integration tests (28 tests passing) - Unit tests for search, tags, quick-add Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
122
src/lib/backlinks.ts
Normal file
122
src/lib/backlinks.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
const BACKLINK_REGEX = /\[\[([^\]]+)\]\]/g
|
||||
|
||||
export function parseBacklinks(content: string): string[] {
|
||||
const matches = content.matchAll(BACKLINK_REGEX)
|
||||
const titles = new Set<string>()
|
||||
|
||||
for (const match of matches) {
|
||||
const title = match[1].trim()
|
||||
if (title) {
|
||||
titles.add(title)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(titles)
|
||||
}
|
||||
|
||||
export async function syncBacklinks(noteId: string, content: string): Promise<void> {
|
||||
const linkedTitles = parseBacklinks(content)
|
||||
|
||||
await prisma.backlink.deleteMany({
|
||||
where: { sourceNoteId: noteId },
|
||||
})
|
||||
|
||||
if (linkedTitles.length === 0) return
|
||||
|
||||
const targetNotes = await prisma.note.findMany({
|
||||
where: {
|
||||
title: { in: linkedTitles },
|
||||
},
|
||||
select: { id: true, title: true },
|
||||
})
|
||||
|
||||
const titleToId = new Map(targetNotes.map((n) => [n.title.toLowerCase(), n.id]))
|
||||
|
||||
const backlinksToCreate: { sourceNoteId: string; targetNoteId: string }[] = []
|
||||
|
||||
for (const title of linkedTitles) {
|
||||
const targetNoteId = titleToId.get(title.toLowerCase())
|
||||
if (targetNoteId && targetNoteId !== noteId) {
|
||||
backlinksToCreate.push({
|
||||
sourceNoteId: noteId,
|
||||
targetNoteId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (backlinksToCreate.length > 0) {
|
||||
await prisma.backlink.createMany({
|
||||
data: backlinksToCreate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface BacklinkWithNote {
|
||||
id: string
|
||||
sourceNoteId: string
|
||||
targetNoteId: string
|
||||
createdAt: string
|
||||
sourceNote: {
|
||||
id: string
|
||||
title: string
|
||||
type: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBacklinksForNote(noteId: string): Promise<BacklinkWithNote[]> {
|
||||
const backlinks = await prisma.backlink.findMany({
|
||||
where: { targetNoteId: noteId },
|
||||
include: {
|
||||
sourceNote: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
return backlinks.map((bl) => ({
|
||||
id: bl.id,
|
||||
sourceNoteId: bl.sourceNoteId,
|
||||
targetNoteId: bl.targetNoteId,
|
||||
createdAt: bl.createdAt.toISOString(),
|
||||
sourceNote: {
|
||||
id: bl.sourceNote.id,
|
||||
title: bl.sourceNote.title,
|
||||
type: bl.sourceNote.type,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getOutgoingLinksForNote(noteId: string): Promise<BacklinkWithNote[]> {
|
||||
const backlinks = await prisma.backlink.findMany({
|
||||
where: { sourceNoteId: noteId },
|
||||
include: {
|
||||
targetNote: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
return backlinks.map((bl) => ({
|
||||
id: bl.id,
|
||||
sourceNoteId: bl.sourceNoteId,
|
||||
targetNoteId: bl.targetNoteId,
|
||||
createdAt: bl.createdAt.toISOString(),
|
||||
sourceNote: {
|
||||
id: bl.targetNote.id,
|
||||
title: bl.targetNote.title,
|
||||
type: bl.targetNote.type,
|
||||
},
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user