graph-flow-types.ts3.6 KBView on GitHub /**
* The type-level bridge between the two universes the graph canvas adapts between: the persisted
* `GraphNode` / `GraphEdge` model and React Flow's runtime `Node` / `Edge`.
*
* TWO INVARIANTS make the adapter total and keep every keyed lookup side-table-free — the same
* two the outbound flow canvas established:
*
* node.id === the `graphNodes` key (graph-LOCAL: the same person is a different node id
* in a different graph, which is what makes fields
* per-graph)
* node.type === 'node' (always the single literal, because a graph has
* exactly ONE node schema — hence one node component,
* never one per domain)
*
* `data.node` is the persisted entry and the single source of truth for what the node holds.
* Everything else on `GraphNodeData` is a derived OVERLAY — hydration, lint, sync state — and is
* NEVER serialized back. That separation is what stops a render-time annotation becoming a
* persisted field the next save writes.
*/
import type { Edge, Node } from '@xyflow/react';
import type { GraphEdge, GraphEdgeKind, GraphNode, GraphSchema } from '@zero/server/graph';
export interface GraphNodeLint {
code: string;
message: string;
}
export type GraphNodeData = {
/** The persisted entry. The only half that is ever written back. */
node: GraphNode;
/** Resolved from the pointed-at document. An overlay: never written back. */
title: string;
/** How to draw its fields — one row per `displayOnCard` field. */
nodeSchema: GraphSchema['nodeSchema'];
/**
* Bound-field values resolved by `hydrateNodes`, when they have arrived.
*
* Kept SEPARATE from `node.derived` rather than merged into it, so the render can say which
* values are live and which came from the cache — and so nothing can accidentally persist a
* hydration result into the document.
*/
hydrated?: Record<string, unknown>;
/** Overlay: a missing document, an unresolved identity, a dangling edge. */
lint?: GraphNodeLint[];
/** Overlay: whether this node's bound fields are current. */
syncState?: 'idle' | 'syncing' | 'stale';
/** True when the pointed-at document no longer resolves. */
missingDocument?: boolean;
/**
* The node's background tint, as a NAME from the closed shared vocabulary — resolved from
* `view.colorField` and that field's `optionColors`. Absent = no tint.
*/
color?: string;
} & Record<string, unknown>;
export type GraphFlowNode = Node<GraphNodeData, 'node'>;
export type GraphFlowEdgeData = {
kind: string;
/** The declared kind, when this graph declares one. Absent means "draw it plain". */
edgeKind?: GraphEdgeKind;
/** The persisted entry, for the rail to edit when the edge is selected. */
edge: GraphEdge;
/**
* This edge is the layout AXIS — the relation the chart is ranked by.
*
* It draws differently on purpose: orthogonal, with a shared horizontal run between a parent
* and its children, because that is what makes a tree read as a tree. Every other relation
* stays a floating curve, which is what keeps an overlay from being mistaken for structure.
*/
hierarchy?: boolean;
} & Record<string, unknown>;
export type GraphFlowEdge = Edge<GraphFlowEdgeData>;
/** The single node type this canvas registers. One schema per graph, so one component. */
export const GRAPH_NODE_TYPE = 'node' as const;
/** The single edge type. Style and arrowhead come from the KIND, not from a second type. */
export const GRAPH_EDGE_TYPE = 'graphEdge' as const;