Skip to content

Instantly share code, notes, and snippets.

@filipecabaco
Last active October 25, 2022 12:58
Show Gist options
  • Save filipecabaco/579ace4ee5719903b712ce5ba50d6d5a to your computer and use it in GitHub Desktop.
Save filipecabaco/579ace4ee5719903b712ce5ba50d6d5a to your computer and use it in GitHub Desktop.

Ecto Vis

Mix.install([
  {:kino, "~> 0.6.2"},
  {:kino_vega_lite, "~> 0.1.1"},
  {:ecto, "~> 3.8"},
  {:libgraph, "~> 0.16.0"}
])

Introspection Code

defmodule EctoIntrospect do
  def introspect(struct) do
    introspect_fields(struct, assocs(struct))
  end

  def introspect_fields(struct, assocs) do
    struct_name = inspect(struct)

    struct.__schema__(:fields)
    |> Enum.map(fn field -> {field, struct.__schema__(:type, field)} end)
    |> Enum.map(fn {field, type} ->
      case Enum.find(assocs, fn {_, _, owner_key, _, _} -> field == owner_key end) do
        nil ->
          {struct_name, field, type}

        {field, inner_struct, type, relationship, cardinality} ->
          case halt?(assocs(inner_struct), struct) do
            true ->
              nil

            false ->
              {struct_name, field, introspect(inner_struct), type, relationship, cardinality}
          end
      end
    end)
    |> Enum.reject(&is_nil/1)
  end

  defp assocs(struct) do
    struct.__schema__(:associations)
    |> Enum.map(&struct.__schema__(:association, &1))
    |> Enum.map(&{&1.field, &1.related, &1.owner_key, &1.relationship, &1.cardinality})
  end

  def halt?(assocs, parent) do
    Enum.any?(assocs, fn {_, assoc, _, _, _} -> assoc == parent end)
  end
end

Visualize Code

defmodule EctoVisualizeSchema do
  def vis(struct) do
    struct
    |> EctoIntrospect.introspect()
    |> Enum.reduce([], &mermaid/2)
    |> Enum.uniq()
    |> Enum.join("\n")
    |> wrap()
    |> Kino.Markdown.new()
  end

  defp mermaid({struct, field, type}, acc) do
    class_def = "class #{struct}"
    type_def = "#{struct} : #{type} #{field}"
    acc ++ [class_def] ++ [type_def]
  end

  defp mermaid({struct, type, fields, field, relationship, cardinality}, acc) do
    relantionship =
      case {relationship, cardinality} do
        {:parent, :one} -> "\"0\" <|-- \"1\""
        {:parent, :many} -> "\"0\" <|-- \"*\""
        {:child, :one} -> "\"0\" --|> \"1\""
        {:child, :many} -> "\"0\" --|> \"*\""
        _ -> ".."
      end

    linked_struct = fields |> hd() |> elem(0)
    class_def = Enum.flat_map(fields, &mermaid(&1, []))
    relationship_def = ["#{struct} #{relantionship} #{linked_struct}"]
    type_def = "#{struct} : #{type} #{field}"
    acc ++ class_def ++ relationship_def ++ [type_def]
  end

  defp wrap(spec),
    do: """
    ```mermaid
    classDiagram
    #{spec}
    ```
    """
end

Demo

defmodule Organization do
  use Ecto.Schema

  schema "organizations" do
    field(:name, :string)
    has_one(:team, Team)
  end
end

defmodule Team do
  use Ecto.Schema

  schema "teams" do
    field(:name, :string)
    has_many(:persons, Person)
  end
end

defmodule Person do
  use Ecto.Schema

  schema "persons" do
    field(:name, :string)
    field(:age, :integer)

    has_one(:team, Team)
    belongs_to(:organization, Organization)
  end
end

EctoVisualizeSchema.vis(Person)
@DaniruKun
Copy link

I ran the exact same code, on a linked Ecto schema in my app, and get this error:

** (KeyError) key :related not found in: %Ecto.Association.HasThrough{cardinality: :many, field: :linked_applications, owner: User, owner_key: :id, through: [:application_links, :application], on_cast: nil, relationship: :child, unique: true, ordered: false}

@filipecabaco
Copy link
Author

weird that it worked 😅 here's the struct https://hexdocs.pm/ecto/Ecto.Association.HasThrough.html so changing from related to relationship.

@DaniruKun
Copy link

Huh, maybe it used to be some kind of alias in an older Ecto version.

@DaniruKun
Copy link

DaniruKun commented Oct 25, 2022

Hmm I tried with the same exact Ecto version, and it still seems to fail on HasThrough associations.
https://hexdocs.pm/ecto/Ecto.Association.HasThrough.html
https://hexdocs.pm/ecto/Ecto.Schema.html#has_many/3-has_many-has_one-through

@filipecabaco
Copy link
Author

Do you have your code open sourced? I just remembered that one of the limitations is the fact that this doesn't check has_many since the halt condition stops when the inner struct sees the upper struct as a "parent" 🤔

@DaniruKun
Copy link

Nah, but I can try to shrink the failing condition and recreate the entity relation in a few models if I have time today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment