Skip to content
๐Ÿš€ This documentation is for unreal-orm 1.0.0 alpha which requires SurrealDB 2.0 SDK. For use with version 1.x, see here.

Mastering Graph Relations

SurrealDB is a Multi-Model database, meaning it thrives as both a Document and a Graph database. While simple Field.record() links are great for one-to-many relations, Relation Tables (Edges) allow for powerful many-to-many structures with their own properties.

In Unreal ORM, relation tables are defined using Table.relation(). They require two special fields: in and out.

Suppose we have User and Project models. We want to track which users are โ€œmembersโ€ of which projects, and include their โ€œroleโ€ on that specific project.

import Table, { Field } from 'unreal-orm';
class MemberOf extends Table.relation({
name: 'member_of',
fields: {
// The source (User)
in: Field.record(() => User),
// The target (Project)
out: Field.record(() => Project),
// Edge property
role: Field.string({ default: surql`"contributor"` }),
joinedAt: Field.datetime({ default: surql`time::now()` }),
},
}) {}

To create a relationship, you create an instance of the edge model just like any other record.

await MemberOf.create(db, {
in: userId,
out: projectId,
role: 'admin',
});

One of the main benefits of using edges is the ability to traverse them easily in both directions.

You can hydrate the in or out records automatically during a select:

const memberships = await MemberOf.select(db, {
where: surql`out = ${projectId}`,
fetch: ['in'], // Hydrate the User record
});
console.log(memberships[0].in.name); // "Alice"

SurrealDB allows you to query directly from a relationship path. This is often faster and more efficient than using a WHERE clause because it uses graph indices to jump directly between records.

In Unreal ORM, you can modify the from option in select to use a graph path.

// Find all projects that Alice is a member of
// This traverses: User (Alice) -> MemberOf Edge -> Project
const aliceProjects = await Project.select(db, {
from: surql`user:alice->member_of->project`,
});

When selecting related data, you can use the typed<T>() helper to ensure the results are correctly typed in TypeScript.

You can project data across relations directly in the select block.

import { typed } from 'unreal-orm';
const projects = await Project.select(db, {
select: {
name: true,
// Project the count of members using a graph expression
memberCount: typed<number>(surql`count(<-member_of)`),
// Fetch the names of all members
memberNames: typed<string[]>(surql`<-member_of<-user.name`),
},
});
// projects[0].memberCount is typed as number
// projects[0].memberNames is typed as string[]

For a deep dive into how SurrealDB handles graph data, check out the official documentation:


FeatureField.record (Link)Table.relation (Edge)
CardinalityOne-to-One / One-to-ManyMany-to-Many
PropertiesNo (Link only)โœ… Yes (Properties on the Edge)
Query SpeedFast (Direct lookup)Fast (Index-backed graph traversal)
ComplexitySimpleMore setup (Extra table)

Recommendation: Use record links for simple ownership (e.g., Post has one Author). Use relation tables when the relationship itself has data (e.g., User follows User with a since timestamp).