graph_kit 0.6.0 copy "graph_kit: ^0.6.0" to clipboard
graph_kit: ^0.6.0 copied to clipboard

A lightweight, in-memory graph library with pattern-based queries and efficient traversal for Dart and Flutter applications.

Graph Kit Logo

graph kit — lightweight typed directed multigraph + pattern queries

A tiny, in-memory, typed directed multigraph with Cypher-inspired pattern queries

Pub Star on Github License: MIT Dart Version Platform Support Open Issues Pull Requests Contributors Last Commit


A tiny, in-memory, typed directed multigraph with:

  • Typed nodes (e.g., Person, Team, Project, Resource)
  • Typed edges (e.g., WORKS_FOR, MANAGES, ASSIGNED_TO, DEPENDS_ON)
  • Multiple relationships between the same nodes
  • A minimal, Cypher-inspired pattern engine for traversal
  • Complete path results with Neo4j-style edge information

See runnable examples in example/bin/ and the sample graph in example/lib/data.dart.

Table of Contents #

Quick Preview #

Graph Kit demo preview
Mini-Cypher query highlighting nodes and edges in the Flutter demo.

Pattern Query Examples #

  • Simple patterns: "user:User"{alice, bob, charlie}
  • Forward patterns: "user-[:MEMBER_OF]->group"
  • Backward patterns: "resource<-[:CAN_ACCESS]-group<-[:MEMBER_OF]-user"{alice, bob}
  • Label filtering: "user:User{label~Admin}"{bob}

Mini-Cypher Reference #

Graph_kit supports a subset of Cypher syntax with some extensions. Here's the complete reference:

Keywords #

Keyword Support Description
MATCH Yes Optional prefix for queries (Cypher compatibility)
RETURN No Results automatically returned as map
WHERE Partial Use {label=value} or {label~substring} instead

Node Syntax #

variable:Type{filters}

Components:

  • variable: Name for results map (required) - e.g., person, user, employee
  • :Type: Filter by node type (optional) - e.g., :Person, :Team
  • {filters}: Label filtering (optional)

Node Examples:

person:Person              # All Person nodes
user                       # All nodes (any type)
manager:Person{label=Bob}  # Person nodes with exact label "Bob"
admin:User{label~Admin}    # User nodes containing "Admin" in label

Edge Syntax #

-[:EDGE_TYPE]->    # Forward relationship
<-[:EDGE_TYPE]-    # Backward relationship

Edge Examples:

person-[:WORKS_FOR]->team           # Forward: person works for team
team<-[:WORKS_FOR]-person           # Backward: people who work for team
user-[:MEMBER_OF]->group-[:CAN_ACCESS]->resource   # Multi-hop

Label Filters #

Filter Example Matches
{label=Bob} Exact match Node with label exactly "Bob"
{label~bob} Contains (case-insensitive) "Bob", "bob", "Bobby", "Bob Smith"

Filter Examples:

person:Person{label=Alice Cooper}     # Exact name match
user:User{label~admin}               # Any admin user
team:Team{label=Engineering}         # Specific team

Complete Pattern Examples #

# Basic queries (with/without MATCH)
person:Person
MATCH person:Person

# Relationships
person:Person-[:WORKS_FOR]->team:Team
MATCH person:Person-[:MANAGES]->team-[:ASSIGNED_TO]->project

# Filtered queries
person:Person{label~Alice}-[:WORKS_FOR]->team
team:Team{label=Engineering}<-[:WORKS_FOR]-person

# Multi-hop traversal
MATCH person:Person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project

Variable Names in Results #

Query results are organized by variable names:

final results = query.match('manager:Person-[:MANAGES]->team:Team');
// Returns: {'manager': {...}, 'team': {...}}

final results2 = query.match('boss:Person-[:MANAGES]->group:Team');
// Returns: {'boss': {...}, 'group': {...}}

Same node type, different roles:

owner:Person-[:OWNS]->project<-[:ASSIGNED_TO]-team<-[:WORKS_FOR]-worker:Person
# Results: {'owner': {...}, 'worker': {...}, 'project': {...}, 'team': {...}}

Syntax Limitations #

Not Supported:

  • Mixed directions in single pattern: person-[:A]->team<-[:B]-other
  • Variable length paths: person-[:KNOWS*1..3]->friend
  • Complex WHERE clauses: WHERE person.age > 25
  • Multiple MATCH statements
  • OPTIONAL MATCH

Workarounds:

  • Use matchMany() for multiple patterns
  • Use label filters instead of WHERE
  • Use matchRows() for path-specific results

Comparison with Cypher #

Feature Real Cypher graph_kit
Mixed directions Yes No
Variable length paths Yes No
Optional matches Yes Via matchMany
WHERE clauses Yes Via label filters

Core concepts #

  • Node
    • Each node has id, type, and label.
    • Example: u1, type=User, label=Mark.
  • Edge types
    • Relationship labels between nodes, used to traverse (e.g., WORKS_FOR, MANAGES).
  • Graph<N extends Node>
    • addNode(n), addEdge(src, edgeType, dst).
    • outNeighbors(srcId, edgeType), inNeighbors(dstId, edgeType).
  • PatternQuery<N extends Node>
    • match(pattern, {startId}) – run a single chain.
    • matchMany([patterns], {startId}) – run multiple independent chains and union results by variable name.

Pattern syntax (mini, Cypher-inspired) #

  • Seeding without IDs: alias:Type
    • Example: 'users:User' seeds the first segment with all nodes whose type == 'User'.
  • Directional edges:
    • Outgoing: -[:EDGE]-> (uses outNeighbors)
    • Incoming: <-[:EDGE]- (uses inNeighbors)
  • Variables (aliases):
    • Each segment name is a key in the returned map.
    • Example: 'users:User-[:MEMBER_OF]->group' returns keys 'users:User' and 'group'.

Quick start #

  1. Build the example graph
import 'package:graph_kit/graph_kit.dart';

final g = Graph<Node>();
// Add your nodes and edges here
final pq = PatternQuery(g);
  1. All users (no IDs needed)
final res = pq.match('users:User');
for (final id in res['users:User'] ?? {}) {
  final n = g.nodesById[id];
  print('$id (${n?.type}: ${n?.label})');
}

Runnable: dart run example/bin/allusers.dart

  1. Users of a group (by group ID)
final res = pq.match('group-[:MEMBER_OF]<-user', startId: 'g_admins');
print(res['user']); // Set of user IDs

Runnable: dart run example/bin/group_users.dart g_admins or by label "Admins".

  1. Resources a person can access through their team
final res = pq.match(
  'person-[:WORKS_FOR]->team-[:HAS_ACCESS]->resource',
  startId: 'alice',
);
print(res['resource']); // Set of resource IDs

Runnable: dart run example/bin/user_assets.dart u1

  1. People who can work on a project (through team assignments)
final res = pq.match(
  'project-[:ASSIGNED_TO]<-team-[:WORKS_FOR]<-person',
  startId: 'web_app_project',
);
print(res['person']);

Design and performance #

  • Traversal from a known ID (startId) is fast:
    • Each hop uses adjacency maps; cost is proportional to the edges visited.
  • Seeding by type (alias:Type) does a one-time node scan to find initial seeds.
    • For small/medium graphs, this is effectively instant; indexing can be added later if needed.
  • matchMany([...]) mirrors “multiple MATCH/OPTIONAL MATCH” lines in Cypher by running several independent chains from the same start and unioning results.

Row-wise pattern results (new) #

For cases where you need to know which variables co-occurred on the same matched path (e.g., which team gives access to which resource), use matchRows():

final rows = pq.matchRows(
  'person-[:WORKS_FOR]->team-[:HAS_ACCESS]->resource',
  startId: 'alice',
);
// rows: [{person: alice, team: engineering, resource: database}, ...]

// Build resource -> teams map from rows
final resourceToTeams = <String, Set<String>>{};
for (final r in rows) {
  final resource = r['resource']!;
  final team = r['team']!;
  resourceToTeams.putIfAbsent(resource, () => <String>{}).add(team);
}

You can union multiple chains while preserving row bindings with matchRowsMany([...]):

final rows = pq.matchRowsMany([
  'person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project-[:USES]->resource',
  // If people can be assigned directly too:
  'person-[:ASSIGNED_TO]->project-[:USES]->resource',
], startId: 'alice');

// Build resource -> projects mapping
final resourceToProjects = <String, Set<String>>{};
for (final r in rows) {
  final resource = r['resource'];
  final project = r['project'];
  if (resource != null && project != null) {
    resourceToProjects.putIfAbsent(resource, () => <String>{}).add(project);
  }
}

Notes:

  • The first segment supports optional :Type and {label=...}/{label~...} filters for seeding.
  • Intermediate segments currently match by structure (alias and edges); type/label filters may be added later if needed.

Complete path results with edges (v0.6.0+) #

For Neo4j-style path results that include both node mappings and complete edge information, use matchPaths():

final paths = pq.matchPaths('person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project');

for (final path in paths) {
  // Nodes: Map<String, String> - variable names to node IDs
  print('Nodes: ${path.nodes}');

  // Edges: List<PathEdge> - ordered connection details
  for (final edge in path.edges) {
    print('Edge: ${edge.from} -[:${edge.type}]-> ${edge.to}');
    print('  Variables: ${edge.fromVariable} -> ${edge.toVariable}');
  }
  print('---');
}

Example output:

Nodes: {person: alice, team: engineering, project: web_app}
Edge: alice -[:WORKS_FOR]-> engineering
  Variables: person -> team
Edge: engineering -[:ASSIGNED_TO]-> web_app
  Variables: team -> project
---
Nodes: {person: alice, team: engineering, project: mobile_app}
Edge: alice -[:WORKS_FOR]-> engineering
  Variables: person -> team
Edge: engineering -[:ASSIGNED_TO]-> mobile_app
  Variables: team -> project
---

Accessing PathEdge properties:

final edge = path.edges.first;
final fromNodeId = edge.from;          // 'alice'
final toNodeId = edge.to;              // 'engineering'
final edgeType = edge.type;            // 'WORKS_FOR'
final fromVar = edge.fromVariable;     // 'person'
final toVar = edge.toVariable;         // 'team'

// Get actual node objects if needed
final fromNode = graph.nodesById[fromNodeId];  // Node(id: 'alice', ...)
final toNode = graph.nodesById[toNodeId];      // Node(id: 'engineering', ...)

Each PathMatch object contains:

  • nodes: Map of variable names to node IDs (same as matchRows())
  • edges: Ordered list of PathEdge objects with complete connection details

Each PathEdge provides:

  • from/to: Source and target node IDs
  • type: Edge type (e.g., 'WORKS_FOR', 'ASSIGNED_TO')
  • fromVariable/toVariable: Variable names from the pattern

Multiple pattern support:

final paths = pq.matchPathsMany([
  'person-[:LEADS]->project',
  'person-[:WORKS_FOR]->team'
], startId: 'alice');

for (final path in paths) {
  print('${path.nodes}');
}

Example output:

{person: alice, project: web_app}
{person: alice, team: engineering}

Use cases:

  • Path visualization: Show complete routes through your graph
  • Audit trails: Track exactly how entities are connected
  • Neo4j migration: Drop-in replacement for Neo4j path results
  • Edge analysis: Understand relationship types in your traversals

Generic traversal utilities (new) #

For BFS-style expansions and subgraph extraction with hop limits, use expandSubgraph from traversal.dart:

import 'package:graph_kit/graph_kit.dart';

final seeds = {'u1'};
final rightward = {'WORKS_FOR', 'ASSIGNED_TO', 'HAS_ACCESS'}; // your edge types

final sub = expandSubgraph(
  g,
  seeds: seeds,
  edgeTypesRightward: rightward,
  forwardHops: 3,
  backwardHops: 0,
);

print('Nodes: ' + sub.nodes.length.toString());
print('Edges: ' + sub.edges.length.toString());

JSON Serialization #

Save and load graphs to/from JSON for persistence and data exchange:

import 'dart:io';
import 'package:graph_kit/graph_kit.dart';

// Build your graph
final graph = Graph<Node>();
graph.addNode(Node(id: 'alice', type: 'User', label: 'Alice',
  properties: {'email': 'alice@example.com', 'active': true}));
graph.addNode(Node(id: 'team1', type: 'Team', label: 'Engineering'));
graph.addEdge('alice', 'MEMBER_OF', 'team1');

// Serialize to JSON
final json = graph.toJson();
final jsonString = graph.toJsonString(pretty: true);

// Save to file
await File('graph.json').writeAsString(jsonString);

// Load from file
final loadedJson = await File('graph.json').readAsString();
final restoredGraph = GraphSerializer.fromJsonString(loadedJson, Node.fromJson);

// Graph is fully restored - queries work immediately
final query = PatternQuery(restoredGraph);
final members = query.match('team<-[:MEMBER_OF]-user', startId: 'team1');
print(members['user']); // {alice}

Examples index #

  • example/bin/allusers.dart – list all users
  • example/bin/group_users.dart – users in a group (by ID or by label)
  • example/bin/user_assets.dart – assets a user can connect to

License #

See LICENSE.

2
likes
0
points
588
downloads

Publisher

verified publishercodealchemist.dev

Weekly Downloads

A lightweight, in-memory graph library with pattern-based queries and efficient traversal for Dart and Flutter applications.

Repository (GitHub)
View/report issues

Topics

#graph #traversal #patterns #cypher #algorithms

License

unknown (license)

More

Packages that depend on graph_kit