Skip to content

Generates a data structure describing the difference between two ecto structs

License

Notifications You must be signed in to change notification settings

peek-travel/ecto_diff

Repository files navigation

EctoDiff

CI Status codecov SourceLevel Hex.pm Version License Dependabot Status

Generates a data structure that describes the differences between two ecto structs. The primary use-case is to track what changed after calling Repo.insert or Repo.update, especially in conjunction with complex or deeply nested cast_assoc associations.

Installation

The package can be installed by adding ecto_diff to your list of dependencies in mix.exs:

def deps do
  [
    {:ecto_diff, "~> 0.2.2"}
  ]
end

Basic Usage

To demonstrate the basic use-case for EctoDiff, let's look at a simple example. Assume you have two related ecto schemas like the following Pet with many Skills. Importantly, we've chosen to cast_assoc the skills in the pet's changeset function, and we've opted to use on_replace: :delete on the has_many skills association.

EctoDiff structs implement the Access behaviour for working with deeply-nested data.

defmodule Pet do
  use Ecto.Schema
  import Ecto.Changeset

  schema("pets") do
    field :name, :string
    field :type, :string, default: "Cat"

    has_many :skills, Skill, on_replace: :delete
  end

  def new(params), do: changeset(%__MODULE__{}, params)
  def update(struct, params), do: changeset(struct, params)

  defp changeset(struct, params) do
    struct
    |> cast(params, [:name, :type])
    |> cast_assoc(:skills)
  end
end

defmodule Skill do
  use Ecto.Schema
  import Ecto.Changeset

  schema("skills") do
    field :name, :string
    field :level, :integer, default: 1

    belongs_to :pet, Pet
  end

  def changeset(struct, params), do: cast(struct, params, [:name, :level])
end

Now let's insert a pet into the database with three initial skills, defaulting to level: 1.

{:ok, initial_pet} =
  %{name: "Spot", skills: [%{name: "Eating"}, %{name: "Sleeping"}, %{name: "Scratching"}]}
  |> Pet.new()
  |> Repo.insert()

Later, we've decided to update this pet's name and it's skills. In this case, we're leaving "eating" alone (no changes), we're increasing "sleeping" to level: 2, we're implicitly deleting "scratching" by not including it in the list (taking advantage of on_replace: :delete), and we're adding a new skill "meowing".

[eating_id, sleeping_id, scratching_id] = Enum.map(initial_pet.skills, & &1.id)

{:ok, updated_pet} =
  initial_pet
  |> Pet.update(%{name: "Spots", skills: [%{id: eating_id}, %{id: sleeping_id, level: 2}, %{name: "Meowing"}]})
  |> Repo.update()

Now we can use EctoDiff to generate a data structure that describes all changes that occurred, making it easy to walk over all changes and act on them if desired.

iex> EctoDiff.diff(initial_pet, updated_pet)

{:ok,
 #EctoDiff<
   struct: Pet,
   primary_key: %{id: 2},
   effect: :changed,
   previous: #Pet<>,
   current: #Pet<>,
   changes: %{
     name: {"Spot", "Spots"},
     skills: [
       #EctoDiff<
         struct: Skill,
         primary_key: %{id: 5},
         effect: :changed,
         previous: #Skill<>,
         current: #Skill<>,
         changes: %{level: {1, 2}}
       >,
       #EctoDiff<
         struct: Skill,
         primary_key: %{id: 6},
         effect: :deleted,
         previous: #Skill<>,
         current: nil,
         changes: %{}
       >,
       #EctoDiff<
         struct: Skill,
         primary_key: %{id: 7},
         effect: :added,
         previous: #Skill<>,
         current: #Skill<>,
         changes: %{id: {nil, 7}, name: {nil, "Meowing"}, pet_id: {nil, 2}}
       >
     ]
   }
 >}

Detailed documentation can be found at https://hexdocs.pm/ecto_diff.