Edges: backlinks and authored connections
An Edge is a directed, typed connection from one Block to another Block or to a tag name. The Block index holds the Edges of every Block next to its row, so the question „which Blocks point at this one?“ needs no scan of the Documents.
Where Edges come from
projectDocument (@skaile/forge-editor/core) fills BlockRow.edges while it projects a Document. One Edge exists per kind and target; a Block never lists the same Edge twice.
kind | origin | Written by |
|---|---|---|
reference | content | a reference chip ([@…]) anywhere in the Block, nested Blocks included |
derivation | content | the Block the Block's Derivation was made from (not an uploaded asset) |
embed | content | a blockEmbed (![[…]]) in the Block |
tag | content | a tag chip or a tag of a Typed block; the target is { tag }, not a Block |
| any other string | meta | an Edge a person added by hand: meta.edges of the Typed block |
A target without a Document resolves to the Document the Block is in. The index never checks that the target exists: an Edge to a deleted Block stays until its source changes.
import { projectDocument } from '@skaile/forge-editor/core'
const [row] = projectDocument(content, 'doc-1')
row.edges
// [{ to: { documentId: 'doc-2', blockId: 'b' }, kind: 'reference', origin: 'content' },
// { to: { tag: 'klima' }, kind: 'tag', origin: 'content' },
// { to: { documentId: 'doc-1', blockId: 'c' }, kind: 'supports', origin: 'meta' }]
Ask for the Edges of a Block
BlockIndex.edges(address, direction, scope) answers BlockEdge[]: the row's Edge with from, the Block it is written in.
direction: 'out'are the Edges written in the Block itself,tagEdges included.direction: 'in'are the Edges of other Blocks that point at it, in Document order ofscope. AtagEdge never points at a Block, so it is never an incoming one.
The backlinks of a Block are its incoming Edges. A Block that does not exist has no Edges ([]), not an error.
Route and client
| Route | GET /api/editor/blocks/:documentId/:blockId/edges?direction=in|out (API.blockEdges, port index) |
| Default | in |
| Client | EditorBackend.index.edges(address, direction?, signal?), optional: a backend without Edges leaves it out |
The route answers only Edges written in Documents the user may read (the same scope as the Workspace search). It answers 403 when the user may not read the Document of the Block, 400 for a direction other than in or out, 401 without a user and 501 without the index port.
Add Edges by hand
The inspector and the details card of a Typed block show the section Kanten (the edges slot of compositeFeature, EdgesSection in both bindings). It lists the Edges of meta.edges with the kind and the title of the target, and it has a form to add one: the kind as free text, then the target, searched with the same list as the [@ popup (the blocks of this Document, and with the index capability those of other Documents). A click on an entry adds the Edge; the remove button next to an Edge takes it out again. A read-only editor shows the list without the form.
meta.edges is a list of { to: { documentId?, blockId }, kind } (AuthoredEdge, META_EDGES_KEY). The key is written through setBlockProperty, so it follows the same rules as every other Block property: in a collaboration session the server writes it into the open Y.Doc. Entries without a block ID or without a kind are ignored by the projection.
The framework-free model is features/composites/edges-model.ts (edgesSectionOf, addAuthoredEdge, removeAuthoredEdge, edgeTargetChoicesOf); the Vue and React components only render it.
The drizzle table
The drizzle adapter writes the Edges to editor_block_edges in the same transaction as the rows of replace:
| Column | |
|---|---|
from_document_id, from_block_id | the Block the Edge is written in |
seq | position in the row's list; with from_document_id the primary key |
to_document_id, to_block_id | the target Block; NULL for a tag Edge |
tag | the tag name of a tag Edge, else NULL |
kind, origin | as in BlockEdgeRow |
An index on (to_document_id, to_block_id) answers the incoming Edges without a scan. blockIndexTables() returns { blocks, edges }, so a host that migrates the tables with drizzle-kit adds both to its schema; bootstrapBlockIndexTables(db) creates both with IF NOT EXISTS. The memory adapter answers from its rows; both pass the same contract suite (test/support/block-index-contract.ts).
Query: filter, sort, group and total Blocks
The DataQuery model, its pseudo-fields and operators per property kind, the evaluator, the BlockIndex.query port method, the route, the client call and the find_blocks tool.
Server routes and authentication
How every editor route authenticates through the identity port, the full route table with status codes, and websocket authentication.