View Source Dagex behaviour (dagex v2.0.0)

The Dagex library is used to allow your business entities to participate in directed, acyclic graphs backed by PostgreSQL's ltree extenstion.

N.B. The callbacks defined in this module are automatically defined for you on your Ecto models when you use Dagex inside them.

Installation

See the instructions in the project's README to perform initial setup of the required database tables and functions.

Adding and manipulating a DAG for your business entity

Let's say your application deals with classifying different types of animals. You may wish to model a set of animals as follows (taken from a post by Kemal Erdogan):

graph TD
  Animal --> Pet
  Animal --> Livestock
  Pet --> Cat
  Pet --> Dog
  Pet --> Sheep
  Dog --> Doberman
  Dog --> Bulldog
  Livestock --> Dog
  Livestock --> Sheep
  Livestock --> Cow

Assuming you have configured Dagex as per the README, we can go ahead and set up the Ecto model needed. First, run mix ecto.gen.migration add_animal_types and then edit the resulting migration file:

defmodule DagexTest.Repo.Migrations.AddAnimalTypes do
  def change do
    create table("animal_types") do
      add :name, :string, null: false
      timestamps()
    end

    create index("animal_types", :name, unique: true)
    Dagex.Migrations.setup_node_type("animal_types", "2.0.0")
  end
end

Run migrations with mix ecto.migrate and then create your model:

defmodule DagexTest.AnimalType do
  use Ecto.Schema
  use Dagex

  schema "animal_types" do
    field :name, :string
    timestamps()
  end
end

Now we can create the animal types and specify their associations in the DAG, and make queries about the graph itself:

iex> alias DagexTest.{AnimalType, Repo}
iex>
iex> # allows us to compare the contents of two lists independent of order
iex> import Assertions, only: [assert_lists_equal: 2]
iex>
iex> # Add a couple of nodes
iex> {:ok, animal} = %AnimalType{name: "Animal"} |> Repo.insert()
iex> {:ok, pet} = %AnimalType{name: "Pet"} |> Repo.insert()
iex>
iex> # Create an association between the nodes
iex> {:edge_created, _edge} = AnimalType.create_edge(animal, pet) |> Repo.dagex_update()
iex>
iex> # Add the remaining nodes and create the associations as per the graph
iex>
iex> {:ok, livestock} = %AnimalType{name: "Livestock"} |> Repo.insert()
iex> {:edge_created, _edge} = AnimalType.create_edge(animal, livestock) |> Repo.dagex_update()
iex>
iex> {:ok, cat} = %AnimalType{name: "Cat"} |> Repo.insert()
iex> {:edge_created, _edge} = AnimalType.create_edge(pet, cat) |> Repo.dagex_update()
iex>
iex> # Note that a node may have multiple parents
iex> {:ok, dog} = %AnimalType{name: "Dog"} |> Repo.insert()
iex> {:edge_created, _edge} = AnimalType.create_edge(pet, dog) |> Repo.dagex_update()
iex> {:edge_created, _edge} = AnimalType.create_edge(livestock, dog) |> Repo.dagex_update()
iex>
iex> {:ok, sheep} = %AnimalType{name: "Sheep"} |> Repo.insert()
iex> {:edge_created, _edge} = AnimalType.create_edge(pet, sheep) |> Repo.dagex_update()
iex> {:edge_created, _edge} = AnimalType.create_edge(livestock, sheep) |> Repo.dagex_update()
iex>
iex> {:ok, cow} = %AnimalType{name: "Cow"} |> Repo.insert()
iex> {:edge_created, _edge} = AnimalType.create_edge(livestock, cow) |> Repo.dagex_update()
iex>
iex> {:ok, doberman} = %AnimalType{name: "Doberman"} |> Repo.insert()
iex> {:edge_created, _edge} = AnimalType.create_edge(dog, doberman) |> Repo.dagex_update()
iex>
iex> {:ok, bulldog} = %AnimalType{name: "Bulldog"} |> Repo.insert()
iex> {:edge_created, _edge} = AnimalType.create_edge(dog, bulldog) |> Repo.dagex_update()
iex>
iex> # we can get the direct children of a node:
iex> children = AnimalType.children(animal) |> Repo.all()
iex> assert_lists_equal(children, [pet, livestock])
iex>
iex> # we can get the direct parents of a node
iex> parents = AnimalType.parents(dog) |> Repo.all()
iex> assert_lists_equal(parents, [pet, livestock])
iex>
iex> # we can get all of the descendants of a node
iex> descendants = AnimalType.descendants(pet) |> Repo.all()
iex> assert_lists_equal(descendants, [cat, dog, sheep, doberman, bulldog])
iex>
iex> # we can also get the descendants including the node itself
iex> descendants = AnimalType.with_descendants(pet) |> Repo.all()
iex> assert_lists_equal(descendants, [pet, cat, dog, sheep, doberman, bulldog])
iex>
iex> # we can get all ancestors of a node
iex> ancestors = AnimalType.ancestors(bulldog) |> Repo.all()
iex> assert_lists_equal(ancestors, [dog, pet, livestock, animal])
iex>
iex> # we can also get the ancestors including the node itself
iex> ancestors = AnimalType.with_ancestors(bulldog) |> Repo.all()
iex> assert_lists_equal(ancestors, [bulldog, dog, pet, livestock, animal])
iex>
iex> # we can determine if node A precedes (i.e. is an ancestor of) node B
iex> true = AnimalType.precedes?(pet, bulldog) |> Repo.exists?()
iex> false = AnimalType.precedes?(sheep, bulldog) |> Repo.exists?()
iex>
iex> # and we can determine if node A succeeds (i.e. is a descendant of) node B
iex> true = AnimalType.succeeds?(bulldog, pet) |> Repo.exists?()
iex> false = AnimalType.succeeds?(bulldog, sheep) |> Repo.exists?()
iex>
iex> # we can also get the possible paths between two nodes
iex> paths = AnimalType.all_paths(animal, sheep) |> Repo.dagex_paths()
iex> assert_lists_equal(paths, [[animal, pet, sheep], [animal, livestock, sheep]])
iex>
iex> # if we remove an edge
iex> {:edge_removed, _edge} = AnimalType.remove_edge(livestock, dog) |> Repo.dagex_update()
iex> # then
iex> false = AnimalType.succeeds?(bulldog, livestock) |> Repo.exists?()
iex> false = AnimalType.precedes?(livestock, bulldog) |> Repo.exists?()
iex>
iex> # and if we remove a node entirely
iex> Repo.delete!(livestock)
iex> # then
iex> [] = AnimalType.ancestors(cow) |> Repo.all()
iex> assert_lists_equal([dog, pet, animal], AnimalType.ancestors(bulldog) |> Repo.all())

Link to this section Summary

Callbacks

Returns a query that can be passed to your application's Ecto repository's Dagex.Repo.dagex_paths/1 function in order to retrieve a list of the possible paths between two nodes. Each path is itself a list starting with the ancestor node, ending with the descendant node, and including each node in the path between the two. Returns an empty list if no path exists between the two nodes.

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are ancestors of the specified child entity.

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are children of the specified parent entity.

Returns a Dagex.Operations.CreateEdge struct to be passed to Dagex.Repo.dagex_update/1 that will attempt to create a new edge in the implementing module's associated DAG.

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are descendants of the specified parent entity.

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are parents of the specified child entity.

Returns a query that selects ancestor only if ancestor is an ancestor of descendant.

Returns a Dagex.Operations.RemoveEdge struct to be passed to Dagex.Repo.dagex_update/1 that will attempt to remove the specified edge from the implementing module's associated DAG.

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities of the type defined in this module that are at the top level of the DAG (i.e. have no other parents.)

Returns a query that selects descendant only if descendant is a descendant of ancestor.

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are ancestors of the specified child entity as well as the child entity itself.

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are descendants of the specified parent entity along with the parent entity itself.

Link to this section Callbacks

Link to this callback

all_paths(ancestor, descendant)

View Source

Specs

all_paths(ancestor :: Ecto.Schema.t(), descendant :: Ecto.Schema.t()) ::
  Ecto.Queryable.t()

Returns a query that can be passed to your application's Ecto repository's Dagex.Repo.dagex_paths/1 function in order to retrieve a list of the possible paths between two nodes. Each path is itself a list starting with the ancestor node, ending with the descendant node, and including each node in the path between the two. Returns an empty list if no path exists between the two nodes.

Specs

ancestors(child :: Ecto.Schema.t()) :: Ecto.Queryable.t()

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are ancestors of the specified child entity.

Specs

children(parent :: Ecto.Schema.t()) :: Ecto.Queryable.t()

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are children of the specified parent entity.

Link to this callback

create_edge(parent, child)

View Source

Specs

create_edge(parent :: Ecto.Schema.t(), child :: Ecto.Schema.t()) ::
  Dagex.Operations.CreateEdge.t()

Returns a Dagex.Operations.CreateEdge struct to be passed to Dagex.Repo.dagex_update/1 that will attempt to create a new edge in the implementing module's associated DAG.

Specs

descendants(parent :: Ecto.Schema.t()) :: Ecto.Queryable.t()

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are descendants of the specified parent entity.

Specs

parents(child :: Ecto.Schema.t()) :: Ecto.Queryable.t()

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are parents of the specified child entity.

Link to this callback

precedes?(ancestor, descendant)

View Source

Specs

precedes?(ancestor :: Ecto.Schema.t(), descendant :: Ecto.Schema.t()) ::
  Ecto.Queryable.t()

Returns a query that selects ancestor only if ancestor is an ancestor of descendant.

Link to this callback

remove_edge(parent, child)

View Source

Specs

remove_edge(parent :: Ecto.Schema.t(), child :: Ecto.Schema.t()) ::
  Dagex.Operations.RemoveEdge.t()

Returns a Dagex.Operations.RemoveEdge struct to be passed to Dagex.Repo.dagex_update/1 that will attempt to remove the specified edge from the implementing module's associated DAG.

Specs

roots() :: Ecto.Queryable.t()

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities of the type defined in this module that are at the top level of the DAG (i.e. have no other parents.)

Link to this callback

succeeds?(descendant, ancestor)

View Source

Specs

succeeds?(descendant :: Ecto.Schema.t(), ancestor :: Ecto.Schema.t()) ::
  Ecto.Queryable.t()

Returns a query that selects descendant only if descendant is a descendant of ancestor.

Specs

with_ancestors(child :: Ecto.Schema.t()) :: Ecto.Queryable.t()

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are ancestors of the specified child entity as well as the child entity itself.

Link to this callback

with_descendants(parent)

View Source

Specs

with_descendants(parent :: Ecto.Schema.t()) :: Ecto.Queryable.t()

Returns a query that can be passed to your application's Ecto repository to retrieve a list of entities that are descendants of the specified parent entity along with the parent entity itself.