Primitives.org.ai

Graph

Traverse relationships with related() and edges()

Digital Objects provides graph traversal capabilities through the related() and edges() methods. Actions serve as directed edges in a graph, enabling powerful relationship queries.

Graph Model

Actions form a directed graph where:

  • Nodes are Things (entities)
  • Edges are Actions (with verb labels)
  • Direction flows from subject to object
         follows
  alice ──────────> bob
    │                │
    │ wrote          │ wrote
    │                │
    ▼                ▼
  post1           post2
    ▲                ▲
    │                │
    └─── likes ──────┘
           charlie

Find Things connected to a given Thing via actions:

async related<T>(
  id: string,
  verb?: string,
  direction?: 'out' | 'in' | 'both',
  options?: ListOptions
): Promise<Thing<T>[]>

Outgoing Relationships

Find Things this entity points to:

// Who does alice follow?
const following = await provider.related(aliceId, 'follow', 'out')
// [{ id: bobId, noun: 'User', data: { name: 'Bob' }, ... }]

// What posts did alice write?
const posts = await provider.related(aliceId, 'write', 'out')

Incoming Relationships

Find Things that point to this entity:

// Who follows bob?
const followers = await provider.related(bobId, 'follow', 'in')
// [{ id: aliceId, noun: 'User', data: { name: 'Alice' }, ... }]

// Who liked this post?
const likers = await provider.related(postId, 'like', 'in')

Bidirectional

Find Things connected in either direction:

// All connections (friends)
const connections = await provider.related(userId, 'connect', 'both')

All Verbs

Omit verb to get relationships of any type:

// All outgoing relationships
const allOutgoing = await provider.related(userId, undefined, 'out')

// All incoming relationships
const allIncoming = await provider.related(userId, undefined, 'in')

// All connections regardless of verb
const allConnected = await provider.related(userId, undefined, 'both')

edges()

Find the Action edges themselves (not the connected Things):

async edges<T>(
  id: string,
  verb?: string,
  direction?: 'out' | 'in' | 'both',
  options?: ListOptions
): Promise<Action<T>[]>

Use Cases for edges()

// Get the edge metadata
const followEdges = await provider.edges(aliceId, 'follow', 'out')
for (const edge of followEdges) {
  console.log(`Followed at ${edge.createdAt}`)
  console.log(`Edge data: ${JSON.stringify(edge.data)}`)
}

// When you need edge timestamps
const likeEdges = await provider.edges(postId, 'like', 'in')
const sortedByRecent = likeEdges.sort(
  (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
)

// When you need edge IDs for deletion
const edges = await provider.edges(userId, 'follow', 'out')
for (const edge of edges) {
  if (edge.object === targetId) {
    await provider.deleteAction(edge.id)
  }
}

Direction

DirectionMeaningQuery
'out'Subject = IDThings I point to
'in'Object = IDThings that point to me
'both'EitherAll connected things
// Visual representation:
//
//   A ──verb──> B
//   │           │
//   │ (subject) │ (object)
//   │           │
// out from A    in to B

// From A's perspective:
await provider.related(A, 'verb', 'out')  // Returns [B]
await provider.related(A, 'verb', 'in')   // Returns []

// From B's perspective:
await provider.related(B, 'verb', 'out')  // Returns []
await provider.related(B, 'verb', 'in')   // Returns [A]

Common Patterns

Social Graph

// Follow someone
await provider.perform('follow', currentUserId, targetUserId)

// Get following list
const following = await provider.related(userId, 'follow', 'out')

// Get followers
const followers = await provider.related(userId, 'follow', 'in')

// Check if following
const followingList = await provider.related(userId, 'follow', 'out')
const isFollowing = followingList.some(u => u.id === targetId)

// Unfollow
const edges = await provider.edges(userId, 'follow', 'out')
const followEdge = edges.find(e => e.object === targetId)
if (followEdge) {
  await provider.deleteAction(followEdge.id)
}

Content Authorship

// Create a post
const post = await provider.create('Post', { title: 'Hello' })
await provider.perform('write', authorId, post.id)

// Get author's posts
const posts = await provider.related(authorId, 'write', 'out')

// Get post's author
const authors = await provider.related(post.id, 'write', 'in')
const author = authors[0] // Assuming single author

Tags/Categories

// Tag a post
await provider.perform('tag', postId, tagId)

// Get all tags for a post
const tags = await provider.related(postId, 'tag', 'out')

// Get all posts with a tag
const posts = await provider.related(tagId, 'tag', 'in')

Comments

// Add a comment
const comment = await provider.create('Comment', {
  content: 'Great post!',
})
await provider.perform('comment', userId, postId, {
  commentId: comment.id,
})

// Get post comments
const commentEdges = await provider.edges(postId, 'comment', 'in')
const comments = await Promise.all(
  commentEdges.map(async (edge) => {
    const commenter = await provider.get(edge.subject!)
    return {
      id: edge.data?.commentId,
      author: commenter?.data,
      createdAt: edge.createdAt,
    }
  })
)

Task Assignment

// Assign a task
await provider.perform('assign', managerId, taskId, {
  assigneeId: workerId,
  priority: 'high',
})

// Get tasks assigned by a manager
const assigned = await provider.related(managerId, 'assign', 'out')

// Get a worker's tasks
const edges = await provider.edges(workerId, 'assign', 'in')
const myTasks = edges
  .filter(e => e.data?.assigneeId === workerId)
  .map(e => provider.get(e.object!))

Mutual Connections

// Find mutual followers
async function mutualFollowers(userA: string, userB: string) {
  const followersA = await provider.related(userA, 'follow', 'in')
  const followersB = await provider.related(userB, 'follow', 'in')

  const followersAIds = new Set(followersA.map(f => f.id))
  return followersB.filter(f => followersAIds.has(f.id))
}

// Find mutual following (friends)
async function mutualFriends(userA: string, userB: string) {
  const followingA = await provider.related(userA, 'follow', 'out')
  const followingB = await provider.related(userB, 'follow', 'out')

  const followingAIds = new Set(followingA.map(f => f.id))
  return followingB.filter(f => followingAIds.has(f.id))
}

Recommendations

// "Users who follow X also follow..."
async function recommendUsers(userId: string) {
  // Get who I follow
  const following = await provider.related(userId, 'follow', 'out')
  const followingIds = new Set(following.map(u => u.id))

  // Get who they follow
  const suggestions = new Map<string, number>()
  for (const user of following) {
    const theirFollowing = await provider.related(user.id, 'follow', 'out')
    for (const suggestion of theirFollowing) {
      if (suggestion.id !== userId && !followingIds.has(suggestion.id)) {
        suggestions.set(
          suggestion.id,
          (suggestions.get(suggestion.id) ?? 0) + 1
        )
      }
    }
  }

  // Return sorted by frequency
  return [...suggestions.entries()]
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .map(([id]) => id)
}

Multi-Hop Traversal

// Friends of friends
async function friendsOfFriends(userId: string) {
  const friends = await provider.related(userId, 'friend', 'both')
  const friendIds = new Set(friends.map(f => f.id))
  friendIds.add(userId) // Exclude self

  const fof = new Set<string>()
  for (const friend of friends) {
    const theirFriends = await provider.related(friend.id, 'friend', 'both')
    for (const fofUser of theirFriends) {
      if (!friendIds.has(fofUser.id)) {
        fof.add(fofUser.id)
      }
    }
  }

  return Promise.all([...fof].map(id => provider.get(id)))
}

Performance Tips

Use Limits

// Limit results for large graphs
const followers = await provider.related(popularUserId, 'follow', 'in', {
  limit: 100,
})

Filter by Verb

// More efficient than filtering all edges
const posts = await provider.related(userId, 'write', 'out')

// Less efficient
const allEdges = await provider.edges(userId, undefined, 'out')
const posts = allEdges.filter(e => e.verb === 'write')

Batch Lookups

// Instead of N+1 queries
const following = await provider.related(userId, 'follow', 'out')
for (const user of following) {
  const theirPosts = await provider.related(user.id, 'write', 'out')
  // ...
}

// Consider caching or parallel fetches
const following = await provider.related(userId, 'follow', 'out')
const allPosts = await Promise.all(
  following.map(user => provider.related(user.id, 'write', 'out'))
)

Type Safety

interface User {
  name: string
  email: string
}

interface Post {
  title: string
  content: string
}

// Typed results
const followers = await provider.related<User>(userId, 'follow', 'in')
followers[0].data.name // Typed as string

const posts = await provider.related<Post>(userId, 'write', 'out')
posts[0].data.title // Typed as string

Next Steps

Was this page helpful?

On this page