hoangsonww / hoangsonww/CollabNote-Fullstack-App

Backlinks & Graph View (WikiLinks, Bidirectional Mentions, Network Map)

Open
#4 0 comments 0 reactions 1 assignee Claimed by @hoangsonww View on GitHub
documentation enhancement good first issue help wanted question
Dominant language
TypeScript
Stars
27
Forks
11
PR merge metrics
No merged PRs in 30d

Description

## Summary

Add first-class **backlinks** and a **graph view** so notes can reference each other using `[[WikiLinks]]` and `@note` mentions. Auto-extract links on save, show **Backlinks** and **Linked Mentions** sidebars, and visualize relationships in an interactive **Graph** (force-directed) across a notebook or filtered subset. Ships with GraphQL and REST, Supabase Realtime updates, and Postgres migrations.

## Why

* Improve knowledge discovery and non-linear navigation (Roam/Obsidian-style).
* Make collaboration clearer: see who referenced a note and why.
* Reduce duplication by surfacing related notes automatically.

**Success metrics (30 days post-launch):**

* ≥40% of active users create ≥1 link.
* ≥25% of note opens involve following a backlink/graph edge.
* Search exits via link follow increase ≥15%.

---

## User Stories

* As a user, I can type `[[Note Title]]` to create a link; if it doesn’t exist, I’m prompted to create it.
* As a user, I see a **Backlinks** panel on a note listing all inbound links with preview snippets.
* As a user, I can open a **Graph** that shows notes as nodes and links as edges; I can filter by tag, text query, or date.
* As a collaborator, I see **Linked Mentions** when other users reference the note in shared workspaces.
* As a user, I get realtime updates when someone links to my open note.

---

## UX

### Editor

* Inline autocompletion when typing `[[...]]` or `@...` (debounced search by title).
* Hover on an inline link reveals a mini-card (title, first 120 chars, last modified).

### Sidebars

* **Backlinks:** grouped by source note, with snippet of surrounding text; click scrolls to the referencing context.
* **Linked Mentions:** shows mentions from shared notes (requires share permission).

### Graph View

* Route: `/graph` and `/graph?noteId=`
* Controls: search, tag filter, neighbor depth (1–3), pin/expand, focus on current note.
* Tooling: D3 force simulation; Material UI for controls.

---

## Data Model (PostgreSQL)

```sql
-- notes (existing) assumed: id (uuid), user_id, title, content, tags, ...
-- new table: normalized, many-to-many between notes
CREATE TABLE note_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
target_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (source_note_id, target_note_id)
);

-- optional helper for fast lookups
CREATE INDEX idx_note_links_source ON note_links(source_note_id);
CREATE INDEX idx_note_links_target ON note_links(target_note_id);

-- optional: store match context for backlink snippets
CREATE TABLE note_link_contexts (
link_id UUID REFERENCES note_links(id) ON DELETE CASCADE,
excerpt TEXT,
offset_start INT,
offset_end INT
);
```

**Parsing rules**

* `[[Note Title]]` resolves by **exact title match**; if multiple, prefer most recently modified; if none, prompt create.
* `@Note Title` acts the same but styled as a mention.
* Future-proof: detect `[[#heading]]` anchors (v2).

---

## API Design

### GraphQL (NestJS)

```graphql
type NoteLink {
id: ID!
sourceNoteId: ID!
targetNoteId: ID!
createdBy: ID!
createdAt: DateTime!
excerpt: String
}

extend type Query {
backlinks(noteId: ID!): [NoteLink!]!
outgoingLinks(noteId: ID!): [NoteLink!]!
graph(noteId: ID, depth: Int = 1, filter: String, tags: [String!]): [Note!]!
}

extend type Mutation {
upsertLinksFromContent(noteId: ID!, content: String!): Boolean!
createNoteFromTitle(title: String!): Note! # used by "create on link"
}
```

### REST (Swagger)

```
GET /links/backlinks/:noteId
GET /links/outgoing/:noteId
POST /links/parse-upsert { noteId, content }
GET /graph?noteId=&depth=&filter=&tags=
```

### Realtime

* Supabase channel `links:note:{noteId}` publishes:

* `link_created`, `link_deleted`, `links_refreshed`
* Editor subscribes for live backlink panel updates.

---

## Backend Implementation (NestJS)

* `LinksModule` with:

* `LinksService.extract(content: string)` → `{ matches: { title, offsetStart, offsetEnd }[] }` (simple lexer; no heavy Markdown parse required)
* `LinksService.resolveOrCreate(title)` → `noteId`
* `LinksService.upsert(noteId, content)` → create/delete rows in `note_links` and `note_link_contexts` within a txn
* Guards: ensure `source_note_id` is owned or shared with editor rights.
* Multi-tenant: sharing rules respected in backlinks/graph queries.

---

## Frontend (Next.js + Vite + MUI)

* Editor enhancements:

* Autocomplete popover on `[[` and `@` with type-ahead (title, recent first).
* On save, call `POST /links/parse-upsert`.
* Sidebars:

* `BacklinksPanel` + `MentionsPanel` (virtualized lists for large sets).
* Graph:

* `/graph`: D3 component with zoom/pan, node drag, focus, and neighbor highlight.
* Deep-link from a note: “Open in Graph”.

---

## Permissions

* Backlinks list only includes sources the viewer can access.
* Graph query filters out private targets/sources not shared with viewer.
* When creating a note via link in a shared space, new note inherits **workspace** context (if applicable) or remains private (prompt user).

---

## Migrations & Seeds

* SQL above as `V2025_10_04__note_links.sql`.
* Backfill job: scan all notes to populate initial links; run idempotently.

---

## Telemetry & Monitoring

* Events: `link_created`, `backlink_opened`, `graph_opened`, `graph_follow_edge`.
* Prometheus: counter for `links_upsert_total`, histogram for parse latency.
* Log parse failure rate <1%.

---

## Tests

* Parser unit tests (edge cases: nested brackets, punctuation, accents).
* Service integration: upsert creates/deletes rows correctly.
* GraphQL e2e: backlinks visibility with/without sharing.
* Frontend RTL: autocomplete, sidebar rendering, graph node click.

---

## Rollout

1. Feature flag `FEATURE_BACKLINKS=true`.
2. Migrate DB; backfill links offline.
3. Enable for internal, then 20% of users; monitor performance.
4. Full rollout + “What’s new” tooltip.

---

## Acceptance Criteria

* Typing `[[Some Note]]` links, saving creates an edge; opening **Some Note** shows a backlink with an excerpt.
* Autocomplete lists existing titles; creating a new one works and opens the new note.
* Graph view loads ≤1s for ≤500 nodes in local tests; interactive filters work.
* Realtime updates: if user A links to B, user B’s open Backlinks panel updates without refresh.
* Permissions enforced: no leakage of private note titles/content.

---

## Tasks

### Backend

* [ ] Create migrations and indices.
* [ ] Implement parser + link upsert service.
* [ ] REST + GraphQL resolvers.
* [ ] Supabase Realtime triggers/publishers.
* [ ] E2E tests (Jest).

### Frontend

* [ ] Editor autocomplete + hover preview.
* [ ] Backlinks & Mentions panels.
* [ ] Graph page + D3 component.
* [ ] Realtime subscriptions.
* [ ] RTL + Cypress coverage.

### DevOps / Docs

* [ ] Env flags: `FEATURE_BACKLINKS`, `REALTIME_ENABLED`.
* [ ] Swagger & README updates.
* [ ] Dashboards/alerts for parse errors.

---

## Estimated Effort (parallelizable)

* Backend: 3–5 days
* Frontend: 5–7 days
* QA/Docs/DevOps: 2 days

---

## Risks & Mitigations

* **Large graphs slow UI:** limit depth by default, virtualize lists, lazy load neighbors.
* **Ambiguous titles:** show note ID in autocomplete tooltip; consider unique slugs (v2).
* **Permission edge cases:** centralize visibility checks in service layer; add e2e coverage.

---

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.