OGM (Object Graph Mapper)

Work with vertices and edges using convenient OGM helpers for graph database operations.

What is OGM?

OGM (Object Graph Mapper) is a pattern that allows you to work with graph databases using object-oriented concepts. Instead of thinking in terms of tables and rows, you work with vertices (nodes) and edges (relationships). arango-typed provides a powerful OGM layer on top of ArangoDB's native graph capabilities.

OGM helps you:

  • Model Relationships: Easily define and manage relationships between entities
  • Traverse Graphs: Navigate through connected data efficiently
  • Find Paths: Discover connections and paths between entities
  • Graph Algorithms: Perform complex graph operations like shortest path, centrality, etc.
  • Type Safety: TypeScript support for graph operations
  • Simplified API: High-level methods that abstract away complex AQL queries

Graph Concepts

Before diving into OGM, it's important to understand key graph concepts:

  • Vertex (Node): An entity in the graph (e.g., a User, Post, or Product)
  • Edge (Relationship): A connection between two vertices (e.g., "follows", "likes", "purchased")
  • Graph: A collection of vertices and edges
  • Direction: Edges can be directed (one-way) or undirected (bidirectional)
    • Outbound: Edges going out from a vertex
    • Inbound: Edges coming into a vertex
    • Any: Both inbound and outbound edges
  • Path: A sequence of vertices connected by edges
  • Traversal: The process of visiting vertices by following edges

Creating a Graph

First, you need to create a graph in ArangoDB. A graph consists of vertex collections and edge collections.

Using ArangoDB Web Interface

You can create graphs using the ArangoDB web interface or programmatically:

Programmatically Creating a Graph

import { getDatabase } from 'arango-typed';

const db = getDatabase();

// Create a graph with vertex and edge collections
await db.createGraph('social', {
  edgeDefinitions: [
    {
      collection: 'friends',
      from: ['users'],
      to: ['users']
    },
    {
      collection: 'follows',
      from: ['users'],
      to: ['users']
    }
  ]
});

Creating a GraphModel

A GraphModel extends the regular Model class with graph-specific methods. It provides an OGM interface for working with vertices and their relationships.

Basic GraphModel

import { getDatabase, graphModel, Schema } from 'arango-typed';

const db = getDatabase();

// Define schema for vertices
const UserSchema = new Schema({
  name: String,
  email: String,
  age: Number
});

// Create GraphModel
const UserGraph = graphModel(db, 'social', 'users', UserSchema);

GraphModel Parameters

  • database: The ArangoDB database instance
  • graphName: Name of the graph (e.g., 'social')
  • collectionName: Name of the vertex collection (e.g., 'users')
  • schema: Schema definition for vertices

Working with Vertices

GraphModel extends Model, so you can use all regular model methods to work with vertices:

Creating Vertices

// Create a user vertex
const alice = await UserGraph.create({
  name: 'Alice',
  email: 'alice@example.com',
  age: 30
});

const bob = await UserGraph.create({
  name: 'Bob',
  email: 'bob@example.com',
  age: 25
});

const charlie = await UserGraph.create({
  name: 'Charlie',
  email: 'charlie@example.com',
  age: 35
});

Finding Vertices

// Find by ID
const user = await UserGraph.findById('users/123');

// Find by query
const users = await UserGraph.find({ age: { $gte: 30 } }).all();

// Find one
const user = await UserGraph.findOne({ email: 'alice@example.com' });

Working with Edges (Relationships)

Edges represent relationships between vertices. You can create, query, and delete edges.

Creating Relationships

Use createRelationship to create an edge between two vertices:

// Alice follows Bob
await UserGraph.createRelationship(
  'users/alice',      // From vertex ID
  'users/bob',        // To vertex ID
  'follows',          // Edge collection name
  {                   // Edge data (optional)
    since: new Date(),
    status: 'active'
  }
);

// Bob follows Alice (bidirectional relationship)
await UserGraph.createRelationship(
  'users/bob',
  'users/alice',
  'follows',
  { since: new Date() }
);

// Alice and Bob are friends
await UserGraph.createRelationship(
  'users/alice',
  'users/bob',
  'friends',
  { since: new Date('2020-01-01') }
);

Deleting Relationships

// Delete a specific relationship
await UserGraph.deleteRelationship('follows', {
  _from: 'users/alice',
  _to: 'users/bob'
});

// Delete all relationships from a vertex
await UserGraph.deleteRelationship('follows', {
  _from: 'users/alice'
});

Traversing Relationships

Traversal is the process of visiting vertices by following edges. arango-typed provides several methods for traversing graphs.

Get Outbound Relationships

Get all vertices that a vertex points to (outgoing edges):

// Get all users that Alice follows
const following = await UserGraph.getOutbound('users/alice', 'follows');
following.forEach(user => {
  console.log(`${user.name} is followed by Alice`);
});

// With filtering
const activeFollowing = await UserGraph.getOutbound('users/alice', 'follows', {
  filter: { status: 'active' },
  limit: 10
});

Get Inbound Relationships

Get all vertices that point to a vertex (incoming edges):

// Get all users that follow Alice
const followers = await UserGraph.getInbound('users/alice', 'follows');
followers.forEach(user => {
  console.log(`${user.name} follows Alice`);
});

// Count followers
const followerCount = followers.length;

Get Connected Vertices

Get all vertices connected to a vertex (both inbound and outbound):

// Get all users connected to Alice (any direction)
const connected = await UserGraph.getConnected('users/alice', 'follows', {
  direction: 'any',
  limit: 50
});

// Get friends (bidirectional relationship)
const friends = await UserGraph.getConnected('users/alice', 'friends', {
  direction: 'any'
});

Count Relationships

// Count outbound relationships
const followingCount = await UserGraph.countRelationships(
  'users/alice',
  'outbound'
);

// Count inbound relationships
const followerCount = await UserGraph.countRelationships(
  'users/alice',
  'inbound'
);

// Count all relationships
const totalConnections = await UserGraph.countRelationships(
  'users/alice',
  'any'
);

Graph Traversals

For more advanced traversals, use the GraphTraversal class:

Basic Traversal

import { GraphTraversal } from 'arango-typed';

const traversal = new GraphTraversal(
  db,
  'social',              // Graph name
  'users/alice',         // Start vertex
  {
    direction: 'outbound',
    minDepth: 1,
    maxDepth: 2,
    limit: 100
  }
);

// Get all vertices
const vertices = await traversal.vertices();

// Get all edges
const edges = await traversal.edges();

// Get all paths
const paths = await traversal.paths();

Breadth-First Search (BFS)

// Traverse using BFS
const traversal = new GraphTraversal(db, 'social', 'users/alice')
  .direction('outbound')
  .depth(1, 3)
  .bfs(true)
  .limit(50);

const results = await traversal.execute();

Depth-First Search (DFS)

// Traverse using DFS (default)
const traversal = new GraphTraversal(db, 'social', 'users/alice')
  .direction('outbound')
  .depth(1, 5)
  .uniqueVertices('path')
  .limit(100);

const results = await traversal.execute();

Filtered Traversal

// Traverse with filters
const traversal = new GraphTraversal(db, 'social', 'users/alice')
  .direction('outbound')
  .depth(1, 2)
  .filter('vertex.age >= 30')  // Only users 30 or older
  .limit(20);

const matureConnections = await traversal.vertices();

Traversal Options

  • direction: 'outbound' | 'inbound' | 'any'
  • minDepth: Minimum depth to traverse (default: 1)
  • maxDepth: Maximum depth to traverse (default: 1)
  • uniqueVertices: 'none' | 'global' | 'path' - How to handle duplicate vertices
  • uniqueEdges: 'none' | 'global' | 'path' - How to handle duplicate edges
  • bfs: Use breadth-first search (default: false, uses DFS)
  • filter: AQL filter expression
  • limit: Maximum number of results

Path Queries

Path queries help you find connections between vertices. arango-typed provides several path query methods.

Shortest Path

Find the shortest path between two vertices:

import { PathQueries } from 'arango-typed';

const pathQueries = new PathQueries(db, 'social');

// Find shortest path from Alice to Charlie
const path = await pathQueries.shortestPath(
  'users/alice',
  'users/charlie',
  {
    direction: 'any'
  }
);

if (path) {
  console.log('Path found:');
  console.log('Vertices:', path.vertices);
  console.log('Edges:', path.edges);
  console.log('Distance:', path.distance);
} else {
  console.log('No path found');
}

All Paths

Find all paths between two vertices:

// Find all paths from Alice to Charlie
const allPaths = await pathQueries.allPaths(
  'users/alice',
  'users/charlie',
  {
    minDepth: 1,
    maxDepth: 5,
    direction: 'any',
    uniqueVertices: 'global'
  }
);

console.log(`Found ${allPaths.length} paths`);
allPaths.forEach((path, index) => {
  console.log(`Path ${index + 1}: ${path.vertices.join(' -> ')}`);
  console.log(`Distance: ${path.distance}`);
});

K-Shortest Paths

Find the top K shortest paths:

// Find top 3 shortest paths
const topPaths = await pathQueries.kShortestPaths(
  'users/alice',
  'users/charlie',
  3,  // K = 3
  {
    direction: 'any'
  }
);

topPaths.forEach((path, index) => {
  console.log(`Path ${index + 1} (distance: ${path.distance}):`);
  console.log(path.vertices.join(' -> '));
});

Path Exists

Check if a path exists between two vertices:

// Check if Alice can reach Charlie
const exists = await pathQueries.pathExists(
  'users/alice',
  'users/charlie',
  10  // Max depth
);

if (exists) {
  console.log('Path exists');
} else {
  console.log('No path found');
}

Path Distance

Get the distance (number of edges) between two vertices:

// Get path distance
const distance = await pathQueries.pathDistance(
  'users/alice',
  'users/charlie'
);

if (distance !== null) {
  console.log(`Distance: ${distance} edges`);
} else {
  console.log('No path found');
}

Using GraphModel.getPath

GraphModel also provides a getPath method:

// Get path using GraphModel
const path = await UserGraph.getPath(
  'users/alice',
  'users/charlie',
  {
    maxDepth: 10,
    direction: 'any',
    edgeFilter: 'e.status == "active"'  // Only active edges
  }
);

console.log('Vertices:', path.vertices);
console.log('Edges:', path.edges);

Complete Example: Social Network

Here's a complete example of building a social network graph:

import { connect, getDatabase, graphModel, Schema } from 'arango-typed';

// Connect to database
await connect({
  url: 'http://localhost:8529',
  database: 'social_network',
  username: 'root',
  password: ''
});

const db = getDatabase();

// Create graph
try {
  await db.createGraph('social', {
    edgeDefinitions: [
      {
        collection: 'follows',
        from: ['users'],
        to: ['users']
      },
      {
        collection: 'friends',
        from: ['users'],
        to: ['users']
      }
    ]
  });
} catch (error) {
  // Graph might already exist
  console.log('Graph already exists or error:', error);
}

// Define schemas
const UserSchema = new Schema({
  name: String,
  email: String,
  age: Number,
  city: String
});

// Create GraphModel
const UserGraph = graphModel(db, 'social', 'users', UserSchema);

// Create users
const alice = await UserGraph.create({
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
  city: 'New York'
});

const bob = await UserGraph.create({
  name: 'Bob',
  email: 'bob@example.com',
  age: 25,
  city: 'San Francisco'
});

const charlie = await UserGraph.create({
  name: 'Charlie',
  email: 'charlie@example.com',
  age: 35,
  city: 'New York'
});

const diana = await UserGraph.create({
  name: 'Diana',
  email: 'diana@example.com',
  age: 28,
  city: 'Los Angeles'
});

// Create relationships
// Alice follows Bob and Charlie
await UserGraph.createRelationship(alice._id, bob._id, 'follows', {
  since: new Date('2023-01-01')
});

await UserGraph.createRelationship(alice._id, charlie._id, 'follows', {
  since: new Date('2023-02-01')
});

// Bob follows Charlie
await UserGraph.createRelationship(bob._id, charlie._id, 'follows', {
  since: new Date('2023-03-01')
});

// Alice and Bob are friends (bidirectional)
await UserGraph.createRelationship(alice._id, bob._id, 'friends', {
  since: new Date('2020-01-01')
});

// Query relationships
// Who does Alice follow?
const aliceFollowing = await UserGraph.getOutbound(alice._id, 'follows');
console.log('Alice follows:', aliceFollowing.map(u => u.name));

// Who follows Alice?
const aliceFollowers = await UserGraph.getInbound(alice._id, 'follows');
console.log('Alice followers:', aliceFollowers.map(u => u.name));

// Who are Alice's friends?
const aliceFriends = await UserGraph.getConnected(alice._id, 'friends', {
  direction: 'any'
});
console.log('Alice friends:', aliceFriends.map(u => u.name));

// Find path from Alice to Diana
const pathQueries = new PathQueries(db, 'social');
const path = await pathQueries.shortestPath(alice._id, diana._id);
if (path) {
  console.log('Path found:', path.vertices);
} else {
  console.log('No path found');
}

// Traverse Alice's network
const traversal = new GraphTraversal(db, 'social', alice._id)
  .direction('outbound')
  .depth(1, 2)
  .limit(10);

const network = await traversal.vertices();
console.log('Alice network:', network.map(u => u.name));

Graph Algorithms

You can implement various graph algorithms using traversals and path queries:

Finding Mutual Connections

// Find mutual friends between Alice and Bob
const aliceFriends = await UserGraph.getConnected(alice._id, 'friends', {
  direction: 'any'
});

const bobFriends = await UserGraph.getConnected(bob._id, 'friends', {
  direction: 'any'
});

const aliceFriendIds = new Set(aliceFriends.map(f => f._id));
const mutualFriends = bobFriends.filter(f => aliceFriendIds.has(f._id));

console.log('Mutual friends:', mutualFriends.map(f => f.name));

Finding Common Followers

// Find users who follow both Alice and Bob
const aliceFollowers = await UserGraph.getInbound(alice._id, 'follows');
const bobFollowers = await UserGraph.getInbound(bob._id, 'follows');

const aliceFollowerIds = new Set(aliceFollowers.map(f => f._id));
const commonFollowers = bobFollowers.filter(f => aliceFollowerIds.has(f._id));

console.log('Common followers:', commonFollowers.map(f => f.name));

Degree Centrality

// Calculate degree centrality (number of connections)
async function degreeCentrality(userId: string) {
  const outbound = await UserGraph.countRelationships(userId, 'outbound');
  const inbound = await UserGraph.countRelationships(userId, 'inbound');
  return outbound + inbound;
}

const aliceCentrality = await degreeCentrality(alice._id);
console.log(`Alice's degree centrality: ${aliceCentrality}`);

Best Practices

  • Index Edge Collections: Create indexes on _from and _to fields for performance
  • Limit Traversal Depth: Always set reasonable maxDepth limits to avoid infinite loops
  • Use Unique Vertices/Edges: Set appropriate uniqueness options to avoid redundant paths
  • Filter Early: Use filters in traversals to reduce result sets
  • Cache Results: Cache frequently accessed graph queries
  • Batch Operations: When creating many relationships, batch them for better performance
  • Monitor Performance: Use ArangoDB query profiler for slow graph queries
  • Choose BFS vs DFS: Use BFS for finding shortest paths, DFS for exploring all paths
  • Validate Graph Structure: Ensure graphs are properly defined before operations
  • Handle Cycles: Be aware of cycles in graphs and use appropriate uniqueness settings

Performance Optimization

  • Index Edge Collections: Index _from, _to, and frequently queried edge properties
  • Limit Depth: Keep traversal depths reasonable (typically 1-5 for most use cases)
  • Use Limits: Always set limits on traversals to prevent large result sets
  • Filter at Database Level: Use AQL filters instead of filtering in application code
  • Use Shortest Path: For path queries, use shortest path when possible instead of all paths
  • Cache Graph Structure: Cache graph metadata and frequently accessed vertices

Common Use Cases

Social Networks

Model friendships, follows, likes, and other social connections:

// Friend recommendations based on mutual friends
const user = await UserGraph.findById('users/123');
const userFriends = await UserGraph.getConnected(user._id, 'friends', {
  direction: 'any'
});

// Find friends of friends (excluding current friends)
const recommendations = new Set();
for (const friend of userFriends) {
  const friendFriends = await UserGraph.getConnected(friend._id, 'friends', {
    direction: 'any'
  });
  friendFriends.forEach(ff => {
    if (ff._id !== user._id && !userFriends.find(f => f._id === ff._id)) {
      recommendations.add(ff._id);
    }
  });
}

Recommendation Systems

Build recommendation engines based on user behavior and relationships:

// Product recommendations based on similar users
const user = await UserGraph.findById('users/123');
const similarUsers = await UserGraph.getConnected(user._id, 'similar_to', {
  direction: 'any',
  limit: 10
});

// Get products liked by similar users
const recommendations = [];
for (const similarUser of similarUsers) {
  const likedProducts = await ProductGraph.getOutbound(similarUser._id, 'likes');
  recommendations.push(...likedProducts);
}

Knowledge Graphs

Model complex relationships in knowledge bases:

// Find related concepts
const concept = await ConceptGraph.findById('concepts/ai');
const related = await ConceptGraph.getConnected(concept._id, 'related_to', {
  direction: 'any',
  depth: 2
});

Error Handling

Always handle errors when working with graphs:

try {
  const path = await UserGraph.getPath('users/alice', 'users/bob');
  if (path.vertices.length === 0) {
    console.log('No path found');
  } else {
    console.log('Path found:', path.vertices);
  }
} catch (error) {
  console.error('Path query failed:', error);
  // Handle error appropriately
}

Limitations and Considerations

  • Graph Size: Very large graphs may require careful optimization
  • Traversal Performance: Deep traversals can be slow on large graphs
  • Memory Usage: Path queries on large graphs may consume significant memory
  • Graph Definition: Graphs must be properly defined before use
  • Edge Collections: Edge collections must be part of the graph definition
📚 API Reference: For complete API documentation including all methods and TypeScript types, see Graph/OGM Module API Reference.
Next: Learn about Performance for optimization strategies.