Files
Momento/memento-note/lib/notes/sync-note-links.ts
Antigravity e2672cd2c2
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:27:29 +00:00

139 lines
3.9 KiB
TypeScript

import prisma from '@/lib/prisma'
/** [[Titre]] ou [[Titre|noteId]] */
const WIKILINK_RE = /\[\[([^\]|#]+?)(?:\|([^\]|#]+?))?(?:[#][^\]]+)?\]\]/g
const OPEN_NOTE_IN_HREF_RE = /href\s*=\s*["']([^"']*openNote=([^"'&#]+)[^"']*)["']/gi
export interface ParsedNoteLinkTarget {
title: string
noteId?: string
snippet: string
}
export function extractNoteLinkTargets(content: string): ParsedNoteLinkTarget[] {
const results: ParsedNoteLinkTarget[] = []
const seen = new Set<string>()
const push = (title: string, noteId: string | undefined, snippet: string) => {
const key = noteId ? `id:${noteId}` : `title:${title.toLowerCase()}`
if (!title && !noteId) return
if (seen.has(key)) return
seen.add(key)
results.push({ title: title || 'Sans titre', noteId, snippet })
}
const plain = content.replace(/<[^>]+>/g, ' ')
let match: RegExpExecArray | null
WIKILINK_RE.lastIndex = 0
while ((match = WIKILINK_RE.exec(plain)) !== null) {
const title = match[1].trim()
const noteId = match[2]?.trim()
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()
push(title, noteId, snippet)
}
OPEN_NOTE_IN_HREF_RE.lastIndex = 0
while ((match = OPEN_NOTE_IN_HREF_RE.exec(content)) !== null) {
const href = match[1]
const noteId = decodeURIComponent(match[2].trim())
const idx = content.indexOf(href)
const start = Math.max(0, idx - 50)
const end = Math.min(content.length, idx + href.length + 50)
const snippet = content.slice(start, end).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
push('', noteId, snippet)
}
return results
}
/** Parse [[wikilinks]] + liens internes openNote, synchronise la table NoteLink. */
export async function syncNoteLinksForNote(
noteId: string,
userId: string,
content: string,
): Promise<number> {
const note = await prisma.note.findUnique({
where: { id: noteId },
select: { id: true, userId: true },
})
if (!note || note.userId !== userId) return 0
const targets = extractNoteLinkTargets(content)
const upsertedIds: string[] = []
for (const target of targets) {
let targetNoteId = target.noteId
if (targetNoteId) {
const byId = await prisma.note.findFirst({
where: { id: targetNoteId, userId, trashedAt: null },
select: { id: true },
})
if (!byId) {
if (!target.title) continue
targetNoteId = undefined
}
}
if (!targetNoteId && target.title) {
let targetNote = await prisma.note.findFirst({
where: {
userId,
title: { equals: target.title, mode: 'insensitive' },
trashedAt: null,
},
select: { id: true },
})
if (!targetNote) {
targetNote = await prisma.note.create({
data: {
title: target.title,
content: '',
userId,
type: 'richtext',
color: 'default',
isMarkdown: false,
order: 0,
},
select: { id: true },
})
}
targetNoteId = targetNote.id
}
if (!targetNoteId || targetNoteId === noteId) continue
await prisma.noteLink.upsert({
where: {
sourceNoteId_targetNoteId: {
sourceNoteId: noteId,
targetNoteId,
},
},
update: { contextSnippet: target.snippet.slice(0, 200) },
create: {
sourceNoteId: noteId,
targetNoteId,
contextSnippet: target.snippet.slice(0, 200),
},
})
upsertedIds.push(targetNoteId)
}
if (upsertedIds.length > 0) {
await prisma.noteLink.deleteMany({
where: {
sourceNoteId: noteId,
targetNoteId: { notIn: upsertedIds },
},
})
} else {
await prisma.noteLink.deleteMany({ where: { sourceNoteId: noteId } })
}
return upsertedIds.length
}