SHIFT + D

Filter by package

  • Overview
  • Ancestor variants
  • Composition
  • Edge
  • Features
  • Linear numeric
  • Logic
  • Prose
  • Source transform
  • SPA links
  • Utilities

What is Baleada Edge?

Updated on April 5, 2025Source code

Baleada Edge makes it easy to model relational data as an edge list where nodes are data objects and edges are the relationships between them.

It's inspired by DynamoDB, MongoDB, and single-table NoSQL design patterns more generally, but it works with more traditional database tech like SQLite, MySQL, Postgres, etc.

Here's a example of a Baleada Edge list that models polymorphic many-to-many relationships between users, projects, and tasks:

id
from_kind
from
kind
to_kind
to
profile
created_at
updated_at
1
user
1
is
profile
1
{"user": {...}
...
...
2
user
2
is
profile
1
{"user": {...}
...
...
3
project
1
is
profile
1
{"project": {...}
...
...
4
task
1
is
profile
1
{"task": {...}
...
...
5
user
1
owns
task
1
{"user": {...}, "task": {...}}
...
...
6
user
2
is assigned to
task
1
{"user": {...}, "task": {...}}
...
...
7
project
1
involves
task
1
{"project": {...}, "task": {...}}
...
...
8
user
1
leads
project
1
{"user": {...}, "project": {...}}
...
...
9
user
2
collaborates on
project
1
{"user": {...}, "project": {...}}
...
...

Upsides and downsides

The upside of this modeling strategy: Instead of writing glue code (e.g. helper tables, Laravel Eloquent model methods, and bespoke abstractions) to model complex relationships, you just use Baleada Edge to insert a new from/to pair into your edge list. Complex relationships are cheap to create and easy to delete as your app grows and changes.

// In a Laravel app, relate a user to a project:

use Baleada\Edge\Model;

class Edge extends Model {}

Edge::create([
    'from_kind' => 'user',
    'from' => 1,
    'kind' => 'leads',
    'to_kind' => 'project',
    'to' => 1,
    'profile' => [
        'user' => [...],
        'project' => [...],
    ],
]);

When you retrieve data, you can run a single, relatively simple query to get pre-joined results for any access pattern.

// Get all projects led by a specific user:
Edge::from('user', 1)->kind('leads')->to('project')->get();

The downside of this data modeling strategy: profile data for each object is denormalized and duplicated in the profile column across many rows. When you write to the database, you have to update multiple rows to keep the data consistent.

// Update a user's email address
$userProfile = Edge::from('user', 1)
  ->kind('is')
  ->to('profile')
  ->first()
  ->profile->user;

$newUserProfile = [
    ...$userProfile,
    'email' => $newEmail,
];

// Write new data to every edge that references the user's profile
Edge::from('user', 1)->toIn(['profile', 'project', 'task'])
    ->update(['profile->user' => $newUserProfile]);

Also, as relationships get more complex, the queries to retrieve data tend to search nested JSON values, which is less performant than searching traditional columns:

// Store a message sent from one user to another in a chat session
Edge::create([
    'from_kind' => 'user',
    'from' => 1,
    'kind' => 'messaged',
    'to_kind' => 'user',
    'to' => 2,
    'profile' => [
        'userFrom' => [...],
        'userTo' => [...],
        'message' => [...],
        'session' => [...],
    ],
]);

// Get all messages sent to a user during a specific chat session
Edge::to('user', 2)
    ->kind('messaged')
    ->profile('session->id', $sessionId)
    ->get();

Installation

Right now, Baleada Edge supports Laravel, and can be installed with Composer:

composer require baleada/laravel-edge

Usage

Baleada Edge provides two PHP classes: Migration and Model.

Baleada\Edge\Migration extends Laravel's own Migration class. It will create a table with the necessary columns for your edge list.

Baleada\Edge\Model extends Laravel's Eloquent\Model class. It pre-configures $fillable fields and $casts for your edge list, and also comes with a ton of built-in query methods, tailored to the columns created by the Migration class.

With these two tools, you can query complex

Customizing Migration

You can customize the Baleada Edge migration by extending the Migration class.

By default, Migration will create a table named edges. To change this, set the $table property on your class:

use Baleada\Edge\Migration;

return new class extends Migration
{
    protected $table = 'custom_edges';
}

Migration also adds the required columns for your edge list, with some default types. Here's a breakdown of the columns, their purposes, and their default types:

Column
Purpose
Default Type
id
Primary key
id
kind
The kind of the edge, i.e. the nature of the relationship between the from and to nodes.
string
from_kind
The kind of the from node
string
from
The ID of the from node
integer
to_kind
The kind of the to node
string
to
The ID of the to node
integer
profile
A JSON column for additional data
json
created_at
Timestamp for when the edge was created
timestamp
updated_at
Timestamp for when the edge was last updated
timestamp

The types of all columns except profile, created_at, and updated_at can be customized by overriding the columns method in your migration.

columns should return an associative array, where each key is a column name, and each value can be one of two things:

  1. The string name of a Laravel $table method (e.g. string, integer, json, etc.),
  2. OR a callback, whose first parameter is the name of the column it's setting up, and whose second parameter is the table (Illuminate\Database\Schema\Blueprint). (This is considered more dangerous, because you should only use your callback to modify the intended column, but it technically has the power to modify the table in any way.)

Here's an example of a migration that uses string for the from and to columns, and configures from_kind and to_kind to be an enum of user and post:

use Baleada\Edge\Migration;
use Illuminate\Database\Schema\Blueprint;

return new class extends Migration
{
    private $nodeKinds = ['user', 'post'];

    protected function columns(): array
    {
        return [
            // For simple type customizations, just provide
            // the type name:
            'from' => 'string',
            'to' => 'string',
            // For more complex type customizations, provide
            // a callback that receives the column name and
            // the table:
            'from_kind' => fn (
              string $name,
              Blueprint $table
            ) => $table->enum($name, $this->nodeKinds),
            'to_kind' => fn (
              string $name,
              Blueprint $table
            ) => $table->enum($name, $this->nodeKinds),
        ];
    }
}

If you want to add additional columns to your edges table, you can include more keys and values in the associative array returned by columns.

Customizing Model

Just like with Migration, you can customize the Baleada Edge model by extending the Model class.

For example, if you want your edges to use ULIDs instead of the default auto-incrementing integers, you can extend Model and use Laravel's HasUlids trait:

use Baleada\Edge\Model as Model;
use Illuminate\Database\Eloquent\Concerns\HasUlids;

class Edge extends Model
{
    use HasUlids;
}

Querying with Model

Model comes with a ton of built-in query methods, tailored to the columns created by the Migration class.

Here's a breakdown of the most commonly used methods:

Method
Purpose
from($fromKind[, $fromId])
Filter edges by the kind (and optionally the ID) of the from node
to($toKind[, $toId])
Filter edges by the kind (and optionally the ID) of the to node
kind($kind)
Filter by edge kind
profile($key, $value)
Filter by a key-value pair in the profile column

Under the hood, from, to, and kind are thin wrappers around Laravel Eloquent's where method, and profile is a thin wrapper around the whereJsonContains method.

Baleada\Edge\Model also adds thin wrappers around all other Eloquent query methods, like orWhere, whereIn, whereJsonNotContains, etc.:

// Get all edges where the `from` node is a user with ID 1
Edge::from('user', 1)->get();

// Get all edges from users or projects
Edge::from('user')->orFrom('project')->get();

// Get all edges where the `to` node is a post or comment
Edge::toIn(['post', 'comment'])->get();

// Get all edges from users to the tasks they _don't_ own
Edge::from('user')->kindNot('owns')->to('task')->get();

// Get all edges for tasks that are not marked "to-do"
Edge::from('task')
  ->to('profile')
  ->profileNot('task->status', 'to-do')
  ->get();

Connecting multiple edges

In real-world projects modeling data as an edge list, you'll frequently run into use cases where a single network request needs to create multiple edges at the same time. If any one of those edges fails to be created, the data would be inconsistent.

For those cases, call the connect method on your Edge model, passing an array of edge data. connect will return an Eloquent collection of created edges:

use App\Models\Edge;

// When the user is viewing a project and creates a task,
// relate the task to the project and to the user who created it:
$edges = Edge::connect([
  [
    'from_kind' => 'project',
    'from' => $projectId,
    'kind' => 'requires',
    'to_kind' => 'task',
    'to' => $taskId,
    'profile' => [
      'project' => [...],
      'task' => [...],
    ],
  ],
  [
    'from_kind' => 'user',
    'from' => $userId,
    'kind' => 'owns',
    'to_kind' => 'task',
    'to' => $taskId,
    'profile' => [
      'user' => [...],
      'task' => [...],
    ],
  ],
]);

Internally, Baleada Edge uses Laravel's database transactions to ensure that each edges is created with separate queries in a single transaction.

If any one of the queries fails, the entire transaction will be rolled back, and no data will be written to the database.

What is Baleada Composition?What is Baleada Features?

Edit doc on GitHub

ON THIS PAGE

What is Baleada Edge?Upsides and downsidesInstallationUsageCustomizing MigrationCustomizing ModelQuerying with ModelConnecting multiple edges