forked from mirror/crumbs-original
extrcation notes utility for parsing note links
This commit is contained in:
committed by
Fred Cabassut
parent
331c56981d
commit
1ca2ba5b79
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Db } from './db/index.js';
|
||||||
|
import { noteLinks, notes } from './db/schema.js';
|
||||||
|
import { eq, inArray } from 'drizzle-orm';
|
||||||
|
import { canAccessNote } from './api-utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync link rows for a note: removes old associations, inserts the current set.
|
||||||
|
* Mirrors syncNoteTags — called right after a note's content is saved.
|
||||||
|
*/
|
||||||
|
export function syncNoteLinks(db: Db, noteId: string, targetIds: string[]): void {
|
||||||
|
db.delete(noteLinks).where(eq(noteLinks.sourceNoteId, noteId)).run();
|
||||||
|
|
||||||
|
for (const targetId of targetIds) {
|
||||||
|
db.insert(noteLinks).values({ sourceNoteId: noteId, targetNoteId: targetId }).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find notes that link to the given note, scoped to what the requesting user
|
||||||
|
* can access — this both respects normal sharing boundaries and, as a side
|
||||||
|
* effect, prevents an unrelated note from appearing in someone's backlinks
|
||||||
|
* just because it happens to reference a note ID it was never shared to see.
|
||||||
|
*/
|
||||||
|
export function fetchBacklinksForNote(
|
||||||
|
db: Db,
|
||||||
|
noteId: string,
|
||||||
|
userId: number
|
||||||
|
): { id: string; title: string }[] {
|
||||||
|
const linkRows = db
|
||||||
|
.select({ sourceNoteId: noteLinks.sourceNoteId })
|
||||||
|
.from(noteLinks)
|
||||||
|
.where(eq(noteLinks.targetNoteId, noteId))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
if (linkRows.length === 0) return [];
|
||||||
|
|
||||||
|
const sourceIds = linkRows.map((r) => r.sourceNoteId);
|
||||||
|
const accessibleIds = sourceIds.filter((id) => canAccessNote(db, id, userId).canAccess);
|
||||||
|
if (accessibleIds.length === 0) return [];
|
||||||
|
|
||||||
|
const sourceNotes = db
|
||||||
|
.select({ id: notes.id, title: notes.title })
|
||||||
|
.from(notes)
|
||||||
|
.where(inArray(notes.id, accessibleIds))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
return sourceNotes;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { extractNoteLinks } from './note-links.js';
|
||||||
|
|
||||||
|
describe('extractNoteLinks', () => {
|
||||||
|
it('should extract a single note-link reference', () => {
|
||||||
|
expect(extractNoteLinks('See [Grocery List](crumb-note://abc-123)')).toEqual(['abc-123']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract multiple distinct references', () => {
|
||||||
|
const content = '[A](crumb-note://id-1) and [B](crumb-note://id-2)';
|
||||||
|
expect(extractNoteLinks(content)).toEqual(['id-1', 'id-2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deduplicate repeated references', () => {
|
||||||
|
const content = '[A](crumb-note://id-1) again: [A again](crumb-note://id-1)';
|
||||||
|
expect(extractNoteLinks(content)).toEqual(['id-1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array for content with no note-links', () => {
|
||||||
|
expect(extractNoteLinks('Just [a normal link](https://example.com)')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array for empty content', () => {
|
||||||
|
expect(extractNoteLinks('')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match crumb-note:// occurring inside a code block', () => {
|
||||||
|
const content = '```\n[A](crumb-note://id-1)\n```';
|
||||||
|
expect(extractNoteLinks(content)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match crumb-note:// occurring inside inline code', () => {
|
||||||
|
expect(extractNoteLinks('Use `crumb-note://id-1` as an example')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user