grafio
v6.2.0
Published
A graph database with pluggable storage architecture. Supports multiple isolated graphs via graphId partitioning. Ships with a zero-dependency in-memory provider. Includes BFS/DFS traversal, type/property filtering, topological sort, DAG detection, transa
Maintainers
Readme
grafio
A graph database with pluggable storage architecture. Supports multiple isolated graphs via graphId partitioning. Ships with a zero-dependency in-memory provider. Includes BFS/DFS traversal, type/property filtering, topological sort, DAG detection, transaction support, cache management and Mermaid export.
MongoDB Storage: For MongoDB-backed persistence, see the separate
grafio-mongopackage.
Features:
Core
- Async-first API — every method returns
Promise<T> - Directed graphs with typed nodes and relationship-labeled edges
- Multiple graph support via
graphIdpartitioning (isolated graphs in one instance) - Pluggable storage — swap backends without changing application code
Storage Providers
InMemoryStorageProvider— built-in, zero dependencies, perfect for development/testingMongoStorageProvider— available ingrafio-mongopackage
Caching
CachedStorageProvider— wraps any storage provider with LRU/LFU/FIFO cachingCacheManager— manages cache across multiple graphId partitions with budget enforcementGraphManager— application-scoped singleton for cache initializationInMemoryCache— built-in in-process cache (zero dependencies)RedisCache— distributed cache using Redis (via ioredis, optional)
Traversal & Querying
- BFS / DFS traversal — find paths between nodes
- Wildcard traversal —
traverse('*', target),traverse(source, '*'), ortraverse(['a','b'], ['x','y']) - Type filtering — filter by node types (
['Person', 'Company']) and edge types (['KNOWS', 'WORKS_AT']) - Property filtering — O(1) lookup with property value index
- Topological sort — Kahn's algorithm, returns dependency order
- DAG detection — cycle detection for acyclic graph validation
Property Management
- Flat property structure — properties must be supported primitive types only (string, number, boolean, null, undefined)
- Property CRUD — add, update, delete, and clear properties on nodes and edges
- Custom indexes —
createIndex()for property-specific indexes including compound indexes with type
Visualization
- Mermaid export — generate flowchart diagrams from graph data
Data Management
- JSON serialization —
exportJSON()/importJSON()for backup/restore - Deep-frozen properties — immutability guarantees on node/edge data
- Graph factories —
InMemoryGraphFactoryfor controlled instance creation
Transactions
- Atomic multi-operation updates — group multiple operations into a single atomic unit
- Automatic rollback — discard all changes if an error occurs during the transaction
- Copy-on-write snapshots (in-memory) — isolation without blocking
Installation
npm install grafioQuick Start
import { Graph } from 'grafio';
// Create a new graph (uses InMemoryStorageProvider by default)
const graph = new Graph();
// Add nodes — all methods are async, use await
const pythonCourse = await graph.addNode('Course', { name: 'Python', duration: 40 });
const chapter1 = await graph.addNode('Chapter', { name: 'Basics', order: 1 });
const author = await graph.addNode('Author', { name: 'John Doe' });
// Add directed edges with relationship types
await graph.addEdge(pythonCourse.id, chapter1.id, 'CONTAINS');
await graph.addEdge(author.id, pythonCourse.id, 'AUTHOR_OF');
// Navigate the graph
const chapters = await graph.getChildren(pythonCourse.id); // [chapter1]
const courses = await graph.getParents(author.id); // [pythonCourse]
// Find nodes by type
const allCourses = await graph.getNodesByType('Course');
// Find path between nodes
const paths = await graph.traverse(pythonCourse.id, chapter1.id, { method: 'bfs' });
// [[courseId, chapterId]]
// Type-filtered traversal
const filtered = await graph.traverse(author.id, pythonCourse.id, {
nodeTypes: ['Author'],
edgeTypes: ['AUTHOR_OF']
});
// Wildcard traversal — find all authors of a course
const authorPaths = await graph.traverse('*', pythonCourse.id, {
edgeTypes: ['AUTHOR_OF']
});
// [[authorId1, courseId], [authorId2, courseId], ...]
// Find all reachable nodes from a source
const allPaths = await graph.traverse(pythonCourse.id, '*');
// [[courseId, child1], [courseId, child2], ...]
// Check if graph is a DAG
const dag = await graph.isDAG(); // true
// Topological sort
const order = await graph.topologicalSort(); // [authorId, courseId, chapterId]API Reference
All methods return Promise<T> — remember to await every call.
Graph Class
Constructor
new Graph(storageProvider?: IStorageProvider)Omit storageProvider to use the built-in InMemoryStorageProvider.
Node Operations
| Method | Returns | Description |
|--------|---------|-------------|
| addNode(type, properties?, transaction?) | Promise<Node> | Add a new node with type label |
| removeNode(id, cascade?, transaction?) | Promise<boolean> | Remove node; cascade=true removes incident edges |
| getNode(id, transaction?) | Promise<Node \| undefined> | Get node by id |
| hasNode(id, transaction?) | Promise<boolean> | Check if node exists |
| getNodes(transaction?) | Promise<readonly Node[]> | Get all nodes |
| getNodesByType(type, transaction?) | Promise<Node[]> | Get all nodes of a given type |
| getNodesByProperty(key, value, options?) | Promise<Node[]> | Get nodes by property value, optionally filtered by node type |
Node Property Operations
| Method | Returns | Description |
|--------|---------|-------------|
| addNodeProperty(nodeId, key, value, transaction?) | Promise<void> | Add a supported primitive property to a node |
| updateNodeProperty(nodeId, key, value, transaction?) | Promise<void> | Update an existing property on a node |
| deleteNodeProperty(nodeId, key, transaction?) | Promise<void> | Delete a property from a node |
| clearNodeProperties(nodeId, transaction?) | Promise<void> | Remove all properties from a node |
| createIndex('node', propertyKey, type?) | Promise<void> | Create index on node property, optionally compound with type |
Edge Operations
| Method | Returns | Description |
|--------|---------|-------------|
| addEdge(sourceId, targetId, type, properties?, transaction?) | Promise<Edge> | Add a directed edge with relationship type |
| removeEdge(id, transaction?) | Promise<boolean> | Remove edge by id |
| getEdge(id, transaction?) | Promise<Edge \| undefined> | Get edge by id |
| hasEdge(id, transaction?) | Promise<boolean> | Check if edge exists |
| getEdges(transaction?) | Promise<readonly Edge[]> | Get all edges |
| getEdgesByType(type, transaction?) | Promise<Edge[]> | Get all edges of a given relationship type |
| getEdgesByProperty(key, value, options?) | Promise<Edge[]> | Get edges by property value, optionally filtered by edge type |
Edge Property Operations
| Method | Returns | Description |
|--------|---------|-------------|
| addEdgeProperty(edgeId, key, value, transaction?) | Promise<void> | Add a supported primitive property to an edge |
| updateEdgeProperty(edgeId, key, value, transaction?) | Promise<void> | Update an existing property on an edge |
| deleteEdgeProperty(edgeId, key, transaction?) | Promise<void> | Delete a property from an edge |
| clearEdgeProperties(edgeId, transaction?) | Promise<void> | Remove all properties from an edge |
| createIndex('edge', propertyKey, type?) | Promise<void> | Create index on edge property, optionally compound with type |
Navigation
| Method | Returns | Description |
|--------|---------|-------------|
| getParents(nodeId, options?) | Promise<Node[]> | Get parent nodes with optional filters |
| getChildren(nodeId, options?) | Promise<Node[]> | Get child nodes with optional filters |
| getEdgesFrom(sourceId, options?) | Promise<Edge[]> | Get outgoing edges with optional type filter |
| getEdgesTo(targetId, options?) | Promise<Edge[]> | Get incoming edges with optional type filter |
| getDirectEdgesBetween(sourceId, targetId, options?) | Promise<Edge[]> | Get direct edges between two nodes |
Traversal & Analysis
| Method | Returns | Description |
|--------|---------|-------------|
| traverse(sourceId, targetId, opts?, transaction?) | Promise<string[][] \| null> | Find path(s) between nodes using BFS or DFS |
| isDAG() | Promise<boolean> | Check if graph is a Directed Acyclic Graph |
| topologicalSort() | Promise<string[] \| null> | Topological order; null if cycles exist |
| warmCache() | Promise<void> | Pre-warm the cache using configured preloadStrategy |
TraversalOptions Interface
interface TraversalOptions {
method?: 'bfs' | 'dfs'; // default: 'bfs'
nodeTypes?: string[]; // filter traversal to these node types (default: all)
edgeTypes?: string[]; // filter traversal to these edge types (default: all)
maxResults?: number; // limit number of paths (default: 100)
}Wildcard Support
The traverse() method supports wildcards for flexible path finding:
// Find a path to a specific target from every node
await graph.traverse('*', targetId);
// Find a path from a specific source to every reachable target
await graph.traverse(sourceId, '*');
// Find paths for every source/target pair in the graph
await graph.traverse('*', '*');
// Find paths for each combination across two arrays
await graph.traverse(['id1', 'id2'], ['id3', 'id4']);Serialization & Admin
| Method | Returns | Description |
|--------|---------|-------------|
| exportJSON() | Promise<GraphData> | Serialize graph to JSON-compatible object |
| static importJSON(data, storageProvider?) | Promise<Graph> | Reconstruct graph from data |
| clear() | Promise<void> | Remove all nodes and edges |
GraphToMermaid — Mermaid Diagram Generation
Convert your graph to Mermaid flowchart syntax for visualization:
import { Graph, GraphToMermaid } from 'grafio';
const graph = new Graph();
const alice = await graph.addNode('Person', { name: 'Alice' });
const bob = await graph.addNode('Person', { name: 'Bob' });
const carol = await graph.addNode('Person', { name: 'Carol' });
await graph.addEdge(alice.id, bob.id, 'KNOWS');
await graph.addEdge(bob.id, carol.id, 'KNOWS');
await graph.addEdge(alice.id, carol.id, 'FRIEND_OF');
// Async static factory — awaits graph.exportJSON() internally
const mermaid = await GraphToMermaid.fromGraph(graph);
console.log(mermaid.toString());Output:
flowchart TD
abc123["Person | abc123"]
def456["Person | def456"]
ghi789["Person | ghi789"]
abc123 -->|"KNOWS"| def456
def456 -->|"KNOWS"| ghi789
abc123 -->|"FRIEND_OF"| ghi789You can also construct from serialized JSON synchronously:
const json = await graph.exportJSON();
const mermaid = new GraphToMermaid(JSON.stringify(json));
// or
const mermaid2 = new GraphToMermaid(json);GraphToMermaid Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| showProperties | boolean | false | Include node properties in labels |
| includeEdgeLabels | boolean | true | Show edge types on arrows |
| direction | 'TD' \| 'LR' | 'TD' | Flowchart direction (top-down or left-right) |
const mermaid = await GraphToMermaid.fromGraph(graph, {
showProperties: true,
includeEdgeLabels: true,
direction: 'LR'
});Node Class
const node = await graph.addNode('Course', { name: 'Python', duration: 40 });
node.id; // 'uuid-xxxx-xxxx'
node.type; // 'Course'
node.properties; // { name: 'Python', duration: 40 }
node.toJSON(); // { id: '...', type: 'Course', properties: { ... } }Edge Class
const edge = await graph.addEdge(sourceId, targetId, 'CONTAINS', { order: 1 });
edge.id; // 'uuid-xxxx-xxxx'
edge.sourceId; // 'source-node-id'
edge.targetId; // 'target-node-id'
edge.type; // 'CONTAINS'
edge.properties; // { order: 1 }
edge.toJSON(); // { id: '...', sourceId: '...', targetId: '...', type: 'CONTAINS', properties: { order: 1 } }Transactions
Transactions allow batching multiple operations into a single atomic unit. All changes are committed together on success, or discarded on explicit rollback.
import { Graph, GraphTransaction } from 'grafio';
const graph = new Graph();
const txn = graph.createTransaction();
await txn.begin();
try {
const alice = await graph.addNode('Person', { name: 'Alice' }, txn);
const bob = await graph.addNode('Person', { name: 'Bob' }, txn);
await graph.addEdge(alice.id, bob.id, 'KNOWS', {}, txn);
await txn.commit();
} catch (error) {
if (txn.isActive()) {
await txn.rollback(); // Caller must explicitly rollback on failure
}
throw error;
}Transaction lifecycle:
begin()— starts a new transactioncommit()— applies all changes atomically (throws if transaction failed)rollback()— discards all changesisFailed()— returns true if a storage operation failed within the transactionisActive()— returns true if transaction is active and not failed
Transaction-aware methods:
All public Graph methods accept an optional transaction parameter to operate within a transaction context:
// Query within a transaction to see uncommitted changes
const txn = graph.createTransaction();
await txn.begin();
await graph.addNode('Person', { name: 'Alice' }, txn);
// Pass transaction to see uncommitted data
const nodes = await graph.getNodes(txn); // includes Alice
const node = await graph.getNode(nodeId, txn);
const hasIt = await graph.hasNode(nodeId, txn);
// Navigation methods also support transactions and filters via GraphOptions
const parents = await graph.getParents(nodeId, { transaction: txn });
const children = await graph.getChildren(nodeId, { filter: { edgeType: 'KNOWS' }, transaction: txn });Caching
grafio includes a pluggable caching layer that wraps any storage provider for improved read performance.
Quick Start with Caching
import { Graph, GraphManager, CachedStorageProvider, InMemoryCache, CacheConfig } from 'grafio';
import { InMemoryStorageProvider } from 'grafio';
// 1. Initialize GraphManager with cache configuration
GraphManager.init({
cache: {
maxNodesCount: 10000,
maxEdgesCount: 20000,
cacheStore: 'in-memory',
evictionStrategy: 'LRU',
preloadStrategy: 'none',
}
});
// 2. Create a graph (automatically uses caching when configured)
const graph = new Graph(new InMemoryStorageProvider());
// 3. Warm the cache explicitly (for preload strategies other than 'none')
await graph.warmCache();Cache Configuration Options
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| maxNodesCount | number | Maximum total nodes cached across all graphId partitions | 10000 |
| maxEdgesCount | number | Maximum total edges cached across all graphId partitions | 20000 |
| cacheStore | 'in-memory' \| 'redis' | Cache backend type | 'in-memory' |
| evictionStrategy | 'LRU' \| 'LFU' \| 'FIFO' | Per-partition eviction strategy | 'LRU' |
| preloadStrategy | 'none' \| 'all' \| 'recent' \| 'first-n' | Strategy to warm cache on startup | 'none' |
| timestampProperty | string | Property name for 'recent' preload sorting | - |
Preload Strategies
none— Cache starts empty; items populate on first read (default)all— Load all nodes/edges up to budget via storage providerrecent— Load nodes/edges sorted bytimestampProperty(descending). RequirestimestampPropertyfirst-n— Load first N items as returned by storage provider
Using Redis Cache
import { GraphManager } from 'grafio';
import { RedisCache } from 'grafio/cache';
GraphManager.init({
cache: {
maxNodesCount: 50000,
maxEdgesCount: 100000,
cacheStore: 'redis',
evictionStrategy: 'LRU',
preloadStrategy: 'all',
}
});
// RedisCache requires ioredis: npm install ioredisManual Cache Wrapping
For fine-grained control, wrap storage providers manually:
import { CachedStorageProvider, InMemoryStorageProvider, CacheManager, CacheConfig } from 'grafio';
const cacheManager = new CacheManager({ /* CacheConfig */ });
const storage = new InMemoryStorageProvider();
const cachedStorage = new CachedStorageProvider(
storage,
'my-graph-id',
cacheManager,
{ /* CacheConfig */ }
);
const graph = new Graph(cachedStorage);
await cachedStorage.warmCache(); // Preload cacheError Handling
import { Graph, NodeAlreadyExistsError, NodeNotFoundError, NodeHasEdgesError } from 'grafio';
const graph = new Graph();
const alice = await graph.addNode('Person', { name: 'Alice' });
const bob = await graph.addNode('Person', { name: 'Bob' });
await graph.addEdge(alice.id, bob.id, 'KNOWS');
// NodeHasEdgesError — node has incident edges
try {
await graph.removeNode(alice.id); // throws
} catch (e) {
if (e instanceof NodeHasEdgesError) {
console.log(e.message); // "Cannot remove node '...': it still has 1 incident edge(s)..."
}
}
// Cascade delete — removes node AND all incident edges
await graph.removeNode(alice.id, true);
// importJSON validation
try {
const data = {
nodes: [
{ id: 'same-id', type: 'A', properties: {} },
{ id: 'same-id', type: 'B', properties: {} }, // duplicate
],
edges: [],
};
await Graph.importJSON(data); // throws NodeAlreadyExistsError
} catch (e) {
if (e instanceof NodeAlreadyExistsError) {
console.log(e.message); // "Node with id 'same-id' already exists"
}
}Available error classes:
NodeAlreadyExistsErrorEdgeAlreadyExistsErrorNodeNotFoundErrorEdgeNotFoundErrorNodeHasEdgesError— thrown byremoveNode(id)when the node has incident edges andcascadeis nottrueInvalidGraphDataErrorInvalidPropertyError— thrown when property value is not a supported primitive typePropertyAlreadyExistsError— thrown byaddNodeProperty/addEdgePropertywhen property already existsPropertyNotFoundError— thrown byupdateNodeProperty/updateEdgePropertywhen property does not existTransactionNotActiveError— thrown whencommit()orrollback()is called without an active transactionTransactionFailedError— thrown whencommit()is called on a transaction that has been marked as failed
Serialization & Persistence
import { Graph } from 'grafio';
import fs from 'fs/promises';
const graph = new Graph();
// ... build your graph ...
// Export to JSON (async)
const data = await graph.exportJSON();
await fs.writeFile('graph.json', JSON.stringify(data, null, 2));
// Import from JSON (async static method)
const raw = JSON.parse(await fs.readFile('graph.json', 'utf-8'));
const restored = await Graph.importJSON(raw);Cascade Delete
By default, removing a node with incident edges throws NodeHasEdgesError. Pass cascade: true to also remove all incident edges:
const a = await graph.addNode('Person', { name: 'A' });
const b = await graph.addNode('Person', { name: 'B' });
const edge = await graph.addEdge(a.id, b.id, 'KNOWS');
// Without cascade — throws NodeHasEdgesError
try {
await graph.removeNode(a.id);
} catch (e) { /* NodeHasEdgesError */ }
// With cascade — removes node AND edge
await graph.removeNode(a.id, true);
await graph.hasEdge(edge.id); // falsePluggable Storage Architecture
All storage backends implement the IStorageProvider interface:
import type { IStorageProvider } from 'grafio';
class MyCustomProvider implements IStorageProvider {
async insertNode(node: NodeData): Promise<void> { /* ... */ }
async deleteNode(id: string): Promise<void> { /* ... */ }
async hasNode(id: string): Promise<boolean> { /* ... */ }
async getNode(id: string): Promise<NodeData | undefined> { /* ... */ }
async getAllNodes(limit?: number, orderBy?: IOrderBy): Promise<NodeData[]> { /* ... */ }
async getNodesByType(type: string): Promise<NodeData[]> { /* ... */ }
async getNodesByProperty(key: string, value: unknown, nodeType?: string): Promise<NodeData[]> { /* ... */ }
async insertEdge(edge: EdgeData): Promise<void> { /* ... */ }
async deleteEdge(id: string): Promise<void> { /* ... */ }
async hasEdge(id: string): Promise<boolean> { /* ... */ }
async getEdge(id: string): Promise<EdgeData | undefined> { /* ... */ }
async getAllEdges(limit?: number, orderBy?: IOrderBy): Promise<EdgeData[]> { /* ... */ }
async getEdgesByType(type: string): Promise<EdgeData[]> { /* ... */ }
async getEdgesBySource(nodeId: string, type?: string): Promise<EdgeData[]> { /* ... */ }
async getEdgesByTarget(nodeId: string, type?: string): Promise<EdgeData[]> { /* ... */ }
async createIndex(target: 'node' | 'edge', propertyKey: string, type?: string): Promise<void> { /* ... */ }
async addProperty(target: 'node' | 'edge', id: string, key: string, value: unknown): Promise<void> { /* ... */ }
async updateProperty(target: 'node' | 'edge', id: string, key: string, value: unknown): Promise<void> { /* ... */ }
async deleteProperty(target: 'node' | 'edge', id: string, key: string): Promise<void> { /* ... */ }
async clearProperties(target: 'node' | 'edge', id: string): Promise<void> { /* ... */ }
async exportJSON(): Promise<GraphData> { /* ... */ }
async importJSON(data: GraphData): Promise<void> { /* ... */ }
async clear(): Promise<void> { /* ... */ }
}
// IOrderBy interface for ordering collection queries
interface IOrderBy {
field: 'createdOn' | 'updatedOn';
direction: 'asc' | 'desc';
}
const graph = new Graph(new MyCustomProvider());Development
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run tests
npm testTesting
The test suite runs against the built-in InMemoryStorageProvider backend:
In-Memory Tests
tests/graph/Graph.node.test.ts— Node operationstests/graph/Graph.edge.test.ts— Edge operationstests/graph/Graph.traverse.test.ts— BFS/DFS traversaltests/graph/Graph.serialization.test.ts— Serialization round-triptests/graph/Graph.isDAG.test.ts— Cycle detectiontests/graph/Graph.topologicalSort.test.ts— Topological orderingtests/graph/Graph.fromJSON.test.ts— JSON validationtests/graph/Graph.clear.test.ts— Graph clearingtests/graph/Graph.properties.test.ts— Property CRUD and validationtests/graph/GraphToMermaid.test.ts— Mermaid exporttests/graph/Graph.index.test.ts— Index operationstests/graph/Graph.transaction.test.ts— Transaction support
Integration Tests
tests/EducationGraph.inmemory.test.ts— Education graph via InMemory providertests/SocialGraph.inmemory.test.ts— Social graph via InMemory provider
Storage Provider Tests
tests/storage/InMemoryGraphFactory.test.ts— In-memory factory teststests/storage/InMemoryStorageProvider.test.ts— In-memory storage provider unit teststests/storage/CachedStorageProvider.test.ts— Cached storage provider tests (cache optimization, adjacency index, sorting)
Cache Tests
tests/storage/cache/InMemoryCache.test.ts— In-memory cache unit tests (getAll, count, adjacency index)tests/storage/cache/CacheManager.test.ts— Cache manager tests (adjacency index, totalCount)tests/storage/cache/RedisCache.test.ts— Redis cache tests (getAll, count, adjacency index)
Manager Tests
tests/GraphManager.test.ts— Graph manager singleton tests
Contributing
We welcome contributions! Please read our Code of Conduct and Contributing Guide before contributing to help keep our community welcoming and inclusive.
