From 8db4fc6c198783952e10b72ae61d0ec1b8b3f887 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Mon, 28 Mar 2022 15:18:05 -0700 Subject: [PATCH 01/16] add `diff_opts` typespec --- lib/ecto_diff.ex | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/ecto_diff.ex b/lib/ecto_diff.ex index 047a9a8..2d94a17 100644 --- a/lib/ecto_diff.ex +++ b/lib/ecto_diff.ex @@ -52,6 +52,21 @@ defmodule EctoDiff do current: Ecto.Schema.t() } + @typedoc """ + Configurable options for `diff/3`. + + ## Options + + * `:match_key` - A single atom or list of atoms which is used to compare + structs when the `:id` field is either unavailable or unsuitable for + comparison (e.g. can be used to compare structs with a user-provided key, for + instance). The key must exist on all structs to be compared. All structs which + lack the provided key(s) will be omitted from the final diff. + """ + @type diff_opts :: [ + primary_key: atom | [atom] | :unset + ] + defstruct [:struct, :primary_key, :changes, :effect, :previous, :current] @doc """ From f1c788c046fe86bf235f9760c85b9a25f36cf1c9 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Mon, 28 Mar 2022 15:19:00 -0700 Subject: [PATCH 02/16] pass opts down to private funcs --- lib/ecto_diff.ex | 60 +++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/lib/ecto_diff.ex b/lib/ecto_diff.ex index 2d94a17..1f0130b 100644 --- a/lib/ecto_diff.ex +++ b/lib/ecto_diff.ex @@ -166,8 +166,16 @@ defmodule EctoDiff do > """ @spec diff(Ecto.Schema.t() | nil, Ecto.Schema.t() | nil) :: {:ok, t()} | {:ok, :unchanged} - def diff(previous, current) do - diff = do_diff(previous, current) + def diff(previous, current), do: diff(previous, current, []) + + @doc """ + An alternate form of `diff/2` which allows options to be specified. + + See `t:diff_opts/0` for available options. + """ + @spec diff(Ecto.Schema.t() | nil, Ecto.Schema.t() | nil, diff_opts) :: {:ok, t()} | {:ok, :unchanged} + def diff(previous, current, opts) do + diff = do_diff(previous, current, opts) if no_changes?(diff) do {:ok, :unchanged} @@ -176,14 +184,19 @@ defmodule EctoDiff do end end - defp do_diff(nil, %struct{} = current) do + defp do_diff(nil, %struct{} = current, opts) do previous = struct!(struct) diff = do_diff(previous, current) %{diff | effect: :added} end - defp do_diff(%struct{} = previous, nil) do - primary_key_fields = struct.__schema__(:primary_key) + defp do_diff(%struct{} = previous, nil, opts) do + primary_key_fields = + if opts[:match_key] == :unset do + struct.__schema__(:primary_key) + else + List.wrap(opts[:match_key]) + end %__MODULE__{ struct: struct, @@ -195,15 +208,20 @@ defmodule EctoDiff do } end - defp do_diff(%struct{} = previous, %struct{} = current) do - primary_key_fields = struct.__schema__(:primary_key) + defp do_diff(%struct{} = previous, %struct{} = current, opts) do + primary_key_fields = + if opts[:match_key] == :unset do + struct.__schema__(:primary_key) + else + List.wrap(opts[:match_key]) + end field_changes = fields(previous, current) changes = field_changes - |> Map.merge(associations(previous, current)) - |> Map.merge(embeds(previous, current)) + |> Map.merge(associations(previous, current, opts)) + |> Map.merge(embeds(previous, current, opts)) previous_primary_key = Map.take(previous, primary_key_fields) current_primary_key = Map.take(current, primary_key_fields) @@ -245,7 +263,7 @@ defmodule EctoDiff do end end - defp embeds(%struct{} = previous, %struct{} = current) do + defp embeds(%struct{} = previous, %struct{} = current, opts) do embed_names = struct.__schema__(:embeds) embed_names @@ -266,7 +284,7 @@ defmodule EctoDiff do end end - defp associations(%struct{} = previous, %struct{} = current) do + defp associations(%struct{} = previous, %struct{} = current, opts) do association_names = struct.__schema__(:associations) association_names @@ -274,7 +292,7 @@ defmodule EctoDiff do |> Map.new() end - defp association(%struct{} = previous, %struct{} = current, association, acc) do + defp association(%struct{} = previous, %struct{} = current, association, acc, opts) do association_details = struct.__schema__(:association, association) previous_value = Map.get(previous, association) @@ -283,31 +301,31 @@ defmodule EctoDiff do diff_association(previous_value, current_value, association_details, acc) end - defp diff_association(%NotLoaded{}, %NotLoaded{}, %{cardinality: :one} = assoc, acc) do + defp diff_association(%NotLoaded{}, %NotLoaded{}, %{cardinality: :one} = assoc, acc, opts) do diff_association(nil, nil, assoc, acc) end - defp diff_association(%NotLoaded{}, %NotLoaded{}, %{cardinality: :many} = assoc, acc) do + defp diff_association(%NotLoaded{}, %NotLoaded{}, %{cardinality: :many} = assoc, acc, opts) do diff_association([], [], assoc, acc) end - defp diff_association(_previous, %NotLoaded{}, %{field: field}, _acc) do + defp diff_association(_previous, %NotLoaded{}, %{field: field}, _acc, opts) do raise "previously loaded association `#{field}` not loaded in current struct" end - defp diff_association(%NotLoaded{}, current, %{cardinality: :one} = assoc, acc) do + defp diff_association(%NotLoaded{}, current, %{cardinality: :one} = assoc, acc, opts) do diff_association(nil, current, assoc, acc) end - defp diff_association(%NotLoaded{}, current, %{cardinality: :many} = assoc, acc) do + defp diff_association(%NotLoaded{}, current, %{cardinality: :many} = assoc, acc, opts) do diff_association([], current, assoc, acc) end - defp diff_association(nil, nil, %{cardinality: :one}, acc), do: acc + defp diff_association(nil, nil, %{cardinality: :one}, acc, opts), do: acc - defp diff_association([], [], %{cardinality: :many}, acc), do: acc + defp diff_association([], [], %{cardinality: :many}, acc, opts), do: acc - defp diff_association(previous, current, %{cardinality: :one, field: field}, acc) do + defp diff_association(previous, current, %{cardinality: :one, field: field}, acc, opts) do assoc_diff = do_diff(previous, current) if no_changes?(assoc_diff) do @@ -317,7 +335,7 @@ defmodule EctoDiff do end end - defp diff_association(previous, current, %{cardinality: :many, field: field, related: struct}, acc) do + defp diff_association(previous, current, %{cardinality: :many, field: field, related: struct}, acc, opts) do primary_key_fields = struct.__schema__(:primary_key) if primary_key_fields == [], From 7bb6ea07f5f15b96274505bd2e42119b4eda3960 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:57:47 -0700 Subject: [PATCH 03/16] import Config instead of Mix.Config --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index db44a02..814015e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :ecto_diff, ecto_repos: [EctoDiff.Repo] From c4f729b7915c379fa107d6a7dc04ab3e8863625a Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:59:29 -0700 Subject: [PATCH 04/16] use explicit overrides --- lib/ecto_diff.ex | 84 ++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/lib/ecto_diff.ex b/lib/ecto_diff.ex index 1f0130b..76b3ada 100644 --- a/lib/ecto_diff.ex +++ b/lib/ecto_diff.ex @@ -57,16 +57,33 @@ defmodule EctoDiff do ## Options - * `:match_key` - A single atom or list of atoms which is used to compare - structs when the `:id` field is either unavailable or unsuitable for - comparison (e.g. can be used to compare structs with a user-provided key, for - instance). The key must exist on all structs to be compared. All structs which - lack the provided key(s) will be omitted from the final diff. + * `:overrides` - A keyword list or map which provides a reference from a struct (to + be compared) to a key on that struct which will be used as the primary key for + comparison. """ @type diff_opts :: [ - primary_key: atom | [atom] | :unset + overrides: overrides ] + @typedoc """ + A keyword list or a map which specifies a override from an Ecto schema to the desired + primary key, for use in comparing structs. + + Structs that are not specified will be compared using their default primary key. + + ## Examples: + + [{Pet, :refid}, {Skill, :id}] + + or + + %{Skill: :refid, Owner: [:id, :refid]} + """ + @type overrides :: [{Ecto.Schema.t(), primary_key}] | %{Ecto.Schema.t() => primary_key} + + @typedoc "An atom or list of atoms used to define a simple or compound primary key" + @type primary_key :: atom | [atom] + defstruct [:struct, :primary_key, :changes, :effect, :previous, :current] @doc """ @@ -186,17 +203,12 @@ defmodule EctoDiff do defp do_diff(nil, %struct{} = current, opts) do previous = struct!(struct) - diff = do_diff(previous, current) + diff = do_diff(previous, current, opts) %{diff | effect: :added} end defp do_diff(%struct{} = previous, nil, opts) do - primary_key_fields = - if opts[:match_key] == :unset do - struct.__schema__(:primary_key) - else - List.wrap(opts[:match_key]) - end + primary_key_fields = get_primary_key_fields(struct, opts) %__MODULE__{ struct: struct, @@ -209,12 +221,7 @@ defmodule EctoDiff do end defp do_diff(%struct{} = previous, %struct{} = current, opts) do - primary_key_fields = - if opts[:match_key] == :unset do - struct.__schema__(:primary_key) - else - List.wrap(opts[:match_key]) - end + primary_key_fields = get_primary_key_fields(struct, opts) field_changes = fields(previous, current) @@ -267,11 +274,11 @@ defmodule EctoDiff do embed_names = struct.__schema__(:embeds) embed_names - |> Enum.reduce([], &embed(previous, current, &1, &2)) + |> Enum.reduce([], &embed(previous, current, &1, &2, opts)) |> Map.new() end - defp embed(%struct{} = previous, %struct{} = current, embed, acc) do + defp embed(%struct{} = previous, %struct{} = current, embed, acc, opts) do embed_details = struct.__schema__(:embed, embed) previous_embed = Map.get(previous, embed) @@ -280,7 +287,7 @@ defmodule EctoDiff do if is_nil(previous_embed) && is_nil(current_embed) do acc else - diff_association(previous_embed, current_embed, embed_details, acc) + diff_association(previous_embed, current_embed, embed_details, acc, opts) end end @@ -288,7 +295,7 @@ defmodule EctoDiff do association_names = struct.__schema__(:associations) association_names - |> Enum.reduce([], &association(previous, current, &1, &2)) + |> Enum.reduce([], &association(previous, current, &1, &2, opts)) |> Map.new() end @@ -298,35 +305,35 @@ defmodule EctoDiff do previous_value = Map.get(previous, association) current_value = Map.get(current, association) - diff_association(previous_value, current_value, association_details, acc) + diff_association(previous_value, current_value, association_details, acc, opts) end defp diff_association(%NotLoaded{}, %NotLoaded{}, %{cardinality: :one} = assoc, acc, opts) do - diff_association(nil, nil, assoc, acc) + diff_association(nil, nil, assoc, acc, opts) end defp diff_association(%NotLoaded{}, %NotLoaded{}, %{cardinality: :many} = assoc, acc, opts) do - diff_association([], [], assoc, acc) + diff_association([], [], assoc, acc, opts) end - defp diff_association(_previous, %NotLoaded{}, %{field: field}, _acc, opts) do + defp diff_association(_previous, %NotLoaded{}, %{field: field}, _acc, _opts) do raise "previously loaded association `#{field}` not loaded in current struct" end defp diff_association(%NotLoaded{}, current, %{cardinality: :one} = assoc, acc, opts) do - diff_association(nil, current, assoc, acc) + diff_association(nil, current, assoc, acc, opts) end defp diff_association(%NotLoaded{}, current, %{cardinality: :many} = assoc, acc, opts) do - diff_association([], current, assoc, acc) + diff_association([], current, assoc, acc, opts) end - defp diff_association(nil, nil, %{cardinality: :one}, acc, opts), do: acc + defp diff_association(nil, nil, %{cardinality: :one}, acc, _opts), do: acc - defp diff_association([], [], %{cardinality: :many}, acc, opts), do: acc + defp diff_association([], [], %{cardinality: :many}, acc, _opts), do: acc defp diff_association(previous, current, %{cardinality: :one, field: field}, acc, opts) do - assoc_diff = do_diff(previous, current) + assoc_diff = do_diff(previous, current, opts) if no_changes?(assoc_diff) do acc @@ -336,7 +343,7 @@ defmodule EctoDiff do end defp diff_association(previous, current, %{cardinality: :many, field: field, related: struct}, acc, opts) do - primary_key_fields = struct.__schema__(:primary_key) + primary_key_fields = get_primary_key_fields(struct, opts) if primary_key_fields == [], do: raise("cannot determine difference in many association with no primary key for `#{struct}`") @@ -361,7 +368,7 @@ defmodule EctoDiff do prev_child = Map.get(previous_map, key) current_child = Map.get(current_map, key) - do_diff(prev_child, current_child) + do_diff(prev_child, current_child, opts) end) |> Enum.reject(&no_changes?/1) @@ -372,6 +379,15 @@ defmodule EctoDiff do end end + defp get_primary_key_fields(struct, opts) do + overrides = Keyword.get(opts, :overrides, []) + + case overrides[struct] do + nil -> struct.__schema__(:primary_key) + primary_key -> List.wrap(primary_key) + end + end + defp no_changes?(%{effect: :changed, changes: map}) when map == %{}, do: true defp no_changes?(_), do: false From c6e307a62565ad173b56867f7cd8284587ddf29e Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:59:43 -0700 Subject: [PATCH 05/16] add tests for diff/3 --- .../migrations/20190323062915_create_pets.exs | 1 + .../20190323064742_create_owners.exs | 1 + .../20190323165801_create_skills.exs | 1 + test/ecto_diff_test.exs | 169 ++++++++++++++++++ test/support/data_case.ex | 2 +- test/support/test_schemas/owner.ex | 1 + test/support/test_schemas/pet.ex | 1 + test/support/test_schemas/skill.ex | 1 + 8 files changed, 176 insertions(+), 1 deletion(-) diff --git a/priv/repo/migrations/20190323062915_create_pets.exs b/priv/repo/migrations/20190323062915_create_pets.exs index c16f426..bc7fa5b 100644 --- a/priv/repo/migrations/20190323062915_create_pets.exs +++ b/priv/repo/migrations/20190323062915_create_pets.exs @@ -5,6 +5,7 @@ defmodule EctoDiff.Repo.Migrations.CreatePets do create table(:pets) do add :name, :string add :type, :string + add :refid, :uuid end end end diff --git a/priv/repo/migrations/20190323064742_create_owners.exs b/priv/repo/migrations/20190323064742_create_owners.exs index e024aae..b15e793 100644 --- a/priv/repo/migrations/20190323064742_create_owners.exs +++ b/priv/repo/migrations/20190323064742_create_owners.exs @@ -4,6 +4,7 @@ defmodule EctoDiff.Repo.Migrations.CreateOwners do def change do create table(:owners) do add :name, :string + add :refid, :uuid end alter table(:pets) do diff --git a/priv/repo/migrations/20190323165801_create_skills.exs b/priv/repo/migrations/20190323165801_create_skills.exs index 8cea26e..15af654 100644 --- a/priv/repo/migrations/20190323165801_create_skills.exs +++ b/priv/repo/migrations/20190323165801_create_skills.exs @@ -5,6 +5,7 @@ defmodule EctoDiff.Repo.Migrations.CreateSkills do create table(:skills) do add :name, :string add :level, :integer + add :refid, :uuid add :pet_id, references(:pets) end end diff --git a/test/ecto_diff_test.exs b/test/ecto_diff_test.exs index e44df55..f79e70d 100644 --- a/test/ecto_diff_test.exs +++ b/test/ecto_diff_test.exs @@ -3,6 +3,175 @@ defmodule EctoDiffTest do use EctoDiff.DataCase + describe "diff/3" do + test "no changes" do + {:ok, pet} = %{name: "Spot"} |> Pet.new() |> Repo.insert() + + assert {:ok, :unchanged} = EctoDiff.diff(pet, pet, overrides: %{Pet => :refid}) + end + + test "insert" do + {:ok, pet} = %{name: "Spot"} |> Pet.new() |> Repo.insert() + refid = pet.refid + + {:ok, diff} = EctoDiff.diff(nil, pet, overrides: %{Pet => :refid}) + + assert %EctoDiff{ + effect: :added, + primary_key: %{refid: ^refid}, + changes: %{ + id: {nil, _id}, + name: {nil, "Spot"}, + refid: {nil, ^refid} + } + } = diff + end + + test "update" do + {:ok, pet} = %{name: "Spot"} |> Pet.new() |> Repo.insert() + {:ok, updated_pet} = pet |> Pet.update(%{name: "McFluffFace"}) |> Repo.update() + refid = pet.refid + + {:ok, diff} = EctoDiff.diff(pet, updated_pet, overrides: %{Pet => :refid}) + + assert %EctoDiff{ + effect: :changed, + primary_key: %{refid: ^refid}, + changes: %{ + name: {"Spot", "McFluffFace"} + } + } = diff + end + + test "delete" do + {:ok, pet} = %{name: "Spot"} |> Pet.new() |> Repo.insert() + refid = pet.refid + + {:ok, diff} = EctoDiff.diff(pet, nil, overrides: %{Pet => :refid}) + + assert %EctoDiff{ + effect: :deleted, + primary_key: %{refid: ^refid}, + changes: %{} + } = diff + end + + test "insert with belongs_to" do + {:ok, pet} = %{name: "Spot", owner: %{name: "Chris"}} |> Pet.new() |> Repo.insert() + id = pet.id + refid = pet.refid + owner_id = pet.owner.id + + {:ok, diff} = EctoDiff.diff(nil, pet, overrides: [{Pet, :refid}, {Owner, :id}]) + + assert %EctoDiff{ + effect: :added, + primary_key: %{refid: ^refid}, + changes: %{ + id: {nil, ^id}, + name: {nil, "Spot"}, + refid: {nil, ^refid}, + owner_id: {nil, ^owner_id}, + owner: %EctoDiff{ + effect: :added, + primary_key: %{id: ^owner_id}, + changes: %{ + id: {nil, ^owner_id}, + name: {nil, "Chris"} + } + } + } + } = diff + end + + test "insert with multiple association types" do + {:ok, pet} = + %{name: "Spot", skills: [%{name: "Eating"}, %{name: "Sleeping"}], owner: %{name: "Samuel"}} + |> Pet.new() + |> Repo.insert() + + %{ + id: pet_id, + refid: pet_refid, + skills: [ + %{id: eating_id, refid: eating_refid}, + %{id: sleeping_id, refid: sleeping_refid} + ], + owner: %{id: owner_id, refid: owner_refid} + } = pet + + {:ok, diff} = EctoDiff.diff(nil, pet, overrides: %{Pet => :refid, Skill => :refid}) + + assert %EctoDiff{ + effect: :added, + primary_key: %{refid: ^pet_refid}, + changes: %{ + id: {nil, ^pet_id}, + name: {nil, "Spot"}, + refid: {nil, ^pet_refid}, + owner: %EctoDiff{ + effect: :added, + primary_key: %{id: ^owner_id}, + changes: %{ + name: {nil, "Samuel"}, + id: {nil, ^owner_id}, + refid: {nil, ^owner_refid} + } + }, + skills: [ + %EctoDiff{ + effect: :added, + primary_key: %{refid: ^eating_refid}, + changes: %{ + id: {nil, ^eating_id}, + pet_id: {nil, ^pet_id}, + name: {nil, "Eating"}, + refid: {nil, ^eating_refid} + } + }, + %EctoDiff{ + effect: :added, + primary_key: %{refid: ^sleeping_refid}, + changes: %{ + id: {nil, ^sleeping_id}, + pet_id: {nil, ^pet_id}, + name: {nil, "Sleeping"}, + refid: {nil, ^sleeping_refid} + } + } + ] + } + } = diff + end + + test "insert with embeds_one" do + {:ok, pet} = %{name: "Spot", details: %{description: "It's a kitty!"}} |> Pet.new() |> Repo.insert() + id = pet.id + refid = pet.refid + details_id = pet.details.id + + {:ok, diff} = EctoDiff.diff(nil, pet, overrides: [{Pet, :refid}]) + + assert %EctoDiff{ + effect: :added, + primary_key: %{refid: ^refid}, + changes: %{ + id: {nil, ^id}, + name: {nil, "Spot"}, + refid: {nil, ^refid}, + details: %EctoDiff{ + effect: :added, + primary_key: %{id: ^details_id}, + changes: %{ + id: {nil, ^details_id}, + description: {nil, "It's a kitty!"} + } + } + } + } = diff + end + end + describe "diff/2" do test "no changes" do {:ok, pet} = %{name: "Spot"} |> Pet.new() |> Repo.insert() diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 442fb4a..abbd539 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -7,7 +7,7 @@ defmodule EctoDiff.DataCase do using do quote do - alias EctoDiff.{Owner, Pet, Repo} + alias EctoDiff.{Owner, Pet, Repo, Skill} import EctoDiff.DataCase end diff --git a/test/support/test_schemas/owner.ex b/test/support/test_schemas/owner.ex index b5e2fe7..2222b98 100644 --- a/test/support/test_schemas/owner.ex +++ b/test/support/test_schemas/owner.ex @@ -6,6 +6,7 @@ defmodule EctoDiff.Owner do schema("owners") do field :name, :string + field :refid, Ecto.UUID, autogenerate: true end def changeset(struct, params), do: cast(struct, params, [:name]) diff --git a/test/support/test_schemas/pet.ex b/test/support/test_schemas/pet.ex index dcf5e3e..de13c0d 100644 --- a/test/support/test_schemas/pet.ex +++ b/test/support/test_schemas/pet.ex @@ -7,6 +7,7 @@ defmodule EctoDiff.Pet do schema("pets") do field :name, :string field :type, :string, default: "Cat" + field :refid, Ecto.UUID, autogenerate: true belongs_to :owner, EctoDiff.Owner, on_replace: :update has_many :skills, EctoDiff.Skill, on_replace: :delete diff --git a/test/support/test_schemas/skill.ex b/test/support/test_schemas/skill.ex index 099987b..d5d931d 100644 --- a/test/support/test_schemas/skill.ex +++ b/test/support/test_schemas/skill.ex @@ -7,6 +7,7 @@ defmodule EctoDiff.Skill do schema("skills") do field :name, :string field :level, :integer, default: 1 + field :refid, Ecto.UUID, autogenerate: true belongs_to :pet, EctoDiff.Pet end From 37f42094902535adb8c2384d21b9c7cc31b6c4fc Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 11:12:57 -0700 Subject: [PATCH 06/16] update changelog for v0.4.0 --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9d536..0ddafda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Nothing yet +## [0.4.0][] + +- Added `EctoDiff.diff/3` to allow options to be specified when performing a diff. +- Added `:overrides` as an option for diffing. This option allows specification of an alternate primary key for a specific struct in a diff. Unspecified structs will use their default primary key. + ## [0.3.0][] - 2022-02-09 ### Added @@ -49,7 +54,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Initial release -[Unreleased]: https://github.com/peek-travel/ecto_diff/compare/0.3.0...HEAD +[Unreleased]: https://github.com/peek-travel/ecto_diff/compare/0.4.0...HEAD +[0.4.0]: https://github.com/peek-travel/ecto_diff/compare/0.3.0...0.4.0 [0.3.0]: https://github.com/peek-travel/ecto_diff/compare/0.2.2...0.3.0 [0.2.2]: https://github.com/peek-travel/ecto_diff/compare/0.2.1...0.2.2 [0.2.1]: https://github.com/peek-travel/ecto_diff/compare/0.2.0...0.2.1 From 45811d1901ffff909cd2a8b3c81c05737df9cc8b Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 11:15:24 -0700 Subject: [PATCH 07/16] Dependency updates: credo 1.6.3 => 1.6.4 db_connection 2.4.1 => 2.4.2 earmark_parser 1.4.19 => 1.4.25 ecto 3.7.1 => 3.7.2 ex_doc 0.28.0 => 0.28.3 makeup 1.0.5 => 1.1.0 makeup_elixir 0.15.2 => 0.16.0 (minor) nimble_parsec 1.2.1 => 1.2.3 postgrex 0.16.1 => 0.16.2 --- mix.exs | 13 +++++++------ mix.lock | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/mix.exs b/mix.exs index 03927ed..da2b86b 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule EctoDiff.MixProject do use Mix.Project - @version "0.3.0" + @version "0.4.0" @source_url "https://github.com/peek-travel/ecto_diff" def project do @@ -54,6 +54,7 @@ defmodule EctoDiff.MixProject do main: "EctoDiff", source_ref: @version, source_url: @source_url, + main: ["readme"], extras: ["README.md", "CHANGELOG.md", "LICENSE.md"] ] end @@ -80,11 +81,11 @@ defmodule EctoDiff.MixProject do defp deps do [ {:credo, "~> 1.0", only: [:dev, :test], runtime: false}, - {:dialyxir, "~> 1.0.0-rc.5", only: [:dev, :test], runtime: false}, - {:ecto_sql, "~> 3.0", only: [:dev, :test]}, - {:ecto, "~> 3.0"}, - {:ex_doc, "~> 0.18", only: :dev, runtime: false}, - {:excoveralls, "~> 0.10", only: :test}, + {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, + {:ecto_sql, "~> 3.7", only: [:dev, :test]}, + {:ecto, "~> 3.7"}, + {:ex_doc, "~> 0.28", only: :dev, runtime: false}, + {:excoveralls, "~> 0.14", only: :test}, {:jason, ">= 1.0.0", only: [:dev, :test]}, {:postgrex, ">= 0.0.0", only: [:dev, :test]} ] diff --git a/mix.lock b/mix.lock index 21b8aa1..1a4c4d2 100644 --- a/mix.lock +++ b/mix.lock @@ -2,29 +2,29 @@ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "credo": {:hex, :credo, "1.6.3", "0a9f8925dbc8f940031b789f4623fc9a0eea99d3eed600fe831e403eb96c6a83", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1167cde00e6661d740fc54da2ee268e35d3982f027399b64d3e2e83af57a1180"}, - "db_connection": {:hex, :db_connection, "2.4.1", "6411f6e23f1a8b68a82fa3a36366d4881f21f47fc79a9efb8c615e62050219da", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea36d226ec5999781a9a8ad64e5d8c4454ecedc7a4d643e4832bf08efca01f00"}, + "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, + "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, - "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, + "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, - "ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, + "ecto": {:hex, :ecto, "3.7.2", "44c034f88e1980754983cc4400585970b4206841f6f3780967a65a9150ef09a8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a600da5772d1c31abbf06f3e4a1ffb150e74ed3e2aa92ff3cee95901657a874e"}, "ecto_sql": {:hex, :ecto_sql, "3.7.2", "55c60aa3a06168912abf145c6df38b0295c34118c3624cf7a6977cd6ce043081", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c218ea62f305dcaef0b915fb56583195e7b91c91dcfb006ba1f669bfacbff2a"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, + "ex_doc": {:hex, :ex_doc, "0.28.3", "6eea2f69995f5fba94cd6dd398df369fe4e777a47cd887714a0976930615c9e6", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "05387a6a2655b5f9820f3f627450ed20b4325c25977b2ee69bed90af6688e718"}, "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "postgrex": {:hex, :postgrex, "0.16.1", "f94628a32c571266f53cd1e5fca705e626e2417bf1eee6f868985d14e874160a", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6b225df32c857b9430619dbe30200a7ae664e23415a771ae9209396ee8eeee64"}, + "postgrex": {:hex, :postgrex, "0.16.2", "0f83198d0e73a36e8d716b90f45f3bde75b5eebf4ade4f43fa1f88c90a812f74", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a9ea589754d9d4d076121090662b7afe155b374897a6550eb288f11d755acfa0"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, From 8e223d85d0e7e2cbb1b7ca11985a99d961edab20 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 11:33:55 -0700 Subject: [PATCH 08/16] improve typespecs and docs --- lib/ecto_diff.ex | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/ecto_diff.ex b/lib/ecto_diff.ex index 76b3ada..82bf14a 100644 --- a/lib/ecto_diff.ex +++ b/lib/ecto_diff.ex @@ -45,8 +45,8 @@ defmodule EctoDiff do """ @type t :: %__MODULE__{ struct: atom(), - primary_key: %{required(atom()) => any()}, - changes: %{required(atom()) => any()}, + primary_key: %{required(primary_key) => any()}, + changes: %{required(struct_field) => any()}, effect: effect(), previous: Ecto.Schema.t(), current: Ecto.Schema.t() @@ -77,13 +77,20 @@ defmodule EctoDiff do or - %{Skill: :refid, Owner: [:id, :refid]} + %{Skill => :refid, Owner => [:id, :refid]} """ @type overrides :: [{Ecto.Schema.t(), primary_key}] | %{Ecto.Schema.t() => primary_key} - @typedoc "An atom or list of atoms used to define a simple or compound primary key" + @typedoc """ + An atom or list of atoms used to define a simple or compound primary key. + """ @type primary_key :: atom | [atom] + @typedoc """ + A field defined on a struct. + """ + @type struct_field :: atom + defstruct [:struct, :primary_key, :changes, :effect, :previous, :current] @doc """ From f168f74a32af5feb595e16e824cd6e8f246508ec Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 11:57:25 -0700 Subject: [PATCH 09/16] docs improvements --- lib/ecto_diff.ex | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/ecto_diff.ex b/lib/ecto_diff.ex index 82bf14a..a6d0338 100644 --- a/lib/ecto_diff.ex +++ b/lib/ecto_diff.ex @@ -14,8 +14,8 @@ defmodule EctoDiff do @typedoc """ The type of change for a given struct. - Each `t:EctoDiff.t/0` struct will have a field describing what happened to the given ecto struct. The values can be one of - the following: + Each `t:EctoDiff.t/0` struct will have a field describing what happened to the given Ecto schema struct. + The values can be one of the following: * `:added` - This struct is new and was not present previously. This happens when the primary struct, or an associated struct, was added during an update or insert. @@ -45,7 +45,7 @@ defmodule EctoDiff do """ @type t :: %__MODULE__{ struct: atom(), - primary_key: %{required(primary_key) => any()}, + primary_key: %{required(struct_field) => any()}, changes: %{required(struct_field) => any()}, effect: effect(), previous: Ecto.Schema.t(), @@ -82,9 +82,9 @@ defmodule EctoDiff do @type overrides :: [{Ecto.Schema.t(), primary_key}] | %{Ecto.Schema.t() => primary_key} @typedoc """ - An atom or list of atoms used to define a simple or compound primary key. + A struct field or list of fields used to define a simple or composite primary key. """ - @type primary_key :: atom | [atom] + @type primary_key :: struct_field | [struct_field] @typedoc """ A field defined on a struct. @@ -188,6 +188,24 @@ defmodule EctoDiff do ] } > + + Using an override to specify a composite primary key: + + iex> {:ok, pet} = %{name: "Spot", type: "Calico"} |> Pet.new() |> Repo.insert() + iex> {:ok, diff} = EctoDiff.diff(nil, pet, overrides: %{Pet => [:name, :type]}) + iex> diff + #EctoDiff< + struct: Pet, + primary_key: %{name: "Spot", type: "Calico"}, + effect: :added, + previous: #Pet<>, + current: #Pet<>, + changes: %{ + id: {nil, 1}, + name: {nil, "Spot"}, + type: {"Cat", "Calico"} + } + > """ @spec diff(Ecto.Schema.t() | nil, Ecto.Schema.t() | nil) :: {:ok, t()} | {:ok, :unchanged} def diff(previous, current), do: diff(previous, current, []) From 8694632b55c0832db918f0fb83f9a1c587ede421 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 12:29:16 -0700 Subject: [PATCH 10/16] update changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ddafda..b79c15a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [0.4.0][] +### Added + - Added `EctoDiff.diff/3` to allow options to be specified when performing a diff. - Added `:overrides` as an option for diffing. This option allows specification of an alternate primary key for a specific struct in a diff. Unspecified structs will use their default primary key. +### Updated + +- Dependencies: + + - credo 1.6.3 => 1.6.4 + - db_connection 2.4.1 => 2.4.2 + - dialyzer 1.0.0-rc.5 => 1.1.0 + - earmark_parser 1.4.19 => 1.4.25 + - ecto 3.7.1 => 3.7.2 + - ex_doc 0.28.0 => 0.28.3 + - makeup 1.0.5 => 1.1.0 + - makeup_elixir 0.15.2 => 0.16.0 (minor) + - nimble_parsec 1.2.1 => 1.2.3 + - postgrex 0.16.1 => 0.16.2 + ## [0.3.0][] - 2022-02-09 ### Added From 06f446ee6f1e411962bc817b6f35283c13e3c629 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 13:31:12 -0700 Subject: [PATCH 11/16] raise when invalid keys specified for a struct --- lib/ecto_diff.ex | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/ecto_diff.ex b/lib/ecto_diff.ex index a6d0338..c57a8f3 100644 --- a/lib/ecto_diff.ex +++ b/lib/ecto_diff.ex @@ -409,10 +409,25 @@ defmodule EctoDiff do case overrides[struct] do nil -> struct.__schema__(:primary_key) - primary_key -> List.wrap(primary_key) + keys -> validate_keys!(struct, List.wrap(keys)) end end + defp validate_keys!(struct, []) do + raise "no keys specified in override for #{inspect(struct)}" + end + + defp validate_keys!(struct, keys) do + struct_fields = struct.__schema__(:fields) || [] + missing = Enum.filter(keys, &(&1 not in struct_fields)) + + if missing != [] do + raise "the keys #{inspect(missing)} for #{inspect(struct)} are invalid or missing" + end + + keys + end + defp no_changes?(%{effect: :changed, changes: map}) when map == %{}, do: true defp no_changes?(_), do: false From 1c9547700148a2126ddd1c22907205bb6b9e6ed9 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 13:33:48 -0700 Subject: [PATCH 12/16] test an embedded schema --- test/ecto_diff_test.exs | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/test/ecto_diff_test.exs b/test/ecto_diff_test.exs index f79e70d..e618fc1 100644 --- a/test/ecto_diff_test.exs +++ b/test/ecto_diff_test.exs @@ -86,7 +86,12 @@ defmodule EctoDiffTest do test "insert with multiple association types" do {:ok, pet} = - %{name: "Spot", skills: [%{name: "Eating"}, %{name: "Sleeping"}], owner: %{name: "Samuel"}} + %{ + name: "Spot", + skills: [%{name: "Eating"}, %{name: "Sleeping"}], + owner: %{name: "Samuel"}, + details: %{description: "It's a kitty!"} + } |> Pet.new() |> Repo.insert() @@ -97,7 +102,8 @@ defmodule EctoDiffTest do %{id: eating_id, refid: eating_refid}, %{id: sleeping_id, refid: sleeping_refid} ], - owner: %{id: owner_id, refid: owner_refid} + owner: %{id: owner_id, refid: owner_refid}, + details: %{description: description, id: detail_id} } = pet {:ok, diff} = EctoDiff.diff(nil, pet, overrides: %{Pet => :refid, Skill => :refid}) @@ -139,32 +145,13 @@ defmodule EctoDiffTest do refid: {nil, ^sleeping_refid} } } - ] - } - } = diff - end - - test "insert with embeds_one" do - {:ok, pet} = %{name: "Spot", details: %{description: "It's a kitty!"}} |> Pet.new() |> Repo.insert() - id = pet.id - refid = pet.refid - details_id = pet.details.id - - {:ok, diff} = EctoDiff.diff(nil, pet, overrides: [{Pet, :refid}]) - - assert %EctoDiff{ - effect: :added, - primary_key: %{refid: ^refid}, - changes: %{ - id: {nil, ^id}, - name: {nil, "Spot"}, - refid: {nil, ^refid}, + ], details: %EctoDiff{ effect: :added, - primary_key: %{id: ^details_id}, + primary_key: %{id: ^detail_id}, changes: %{ - id: {nil, ^details_id}, - description: {nil, "It's a kitty!"} + id: {nil, ^detail_id}, + description: {nil, ^description} } } } From 00dd40227425c2fa9e2e53fe3a40819ef8536bd8 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 13:33:56 -0700 Subject: [PATCH 13/16] test raising behaviour --- test/ecto_diff_test.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/ecto_diff_test.exs b/test/ecto_diff_test.exs index e618fc1..169e810 100644 --- a/test/ecto_diff_test.exs +++ b/test/ecto_diff_test.exs @@ -157,6 +157,18 @@ defmodule EctoDiffTest do } } = diff end + + test "raises when invalid override keys are specified" do + {:ok, pet} = %{name: "Spot", skills: [%{name: "Karate", level: 6}]} |> Pet.new() |> Repo.insert() + + assert_raise RuntimeError, "the keys [:badkey] for EctoDiff.Skill are invalid or missing", fn -> + {:ok, _diff} = EctoDiff.diff(nil, pet, overrides: %{Skill => [:name, :badkey]}) + end + + assert_raise RuntimeError, "no keys specified in override for EctoDiff.Skill", fn -> + {:ok, _diff} = EctoDiff.diff(nil, pet, overrides: %{Skill => []}) + end + end end describe "diff/2" do From 8553637d0ec88ca9d9d0f30b9b6389fcf7a4e401 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 13:36:01 -0700 Subject: [PATCH 14/16] docs language cleanup --- lib/ecto_diff.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ecto_diff.ex b/lib/ecto_diff.ex index c57a8f3..3f06d96 100644 --- a/lib/ecto_diff.ex +++ b/lib/ecto_diff.ex @@ -57,16 +57,16 @@ defmodule EctoDiff do ## Options - * `:overrides` - A keyword list or map which provides a reference from a struct (to - be compared) to a key on that struct which will be used as the primary key for - comparison. + * `:overrides` - A keyword list or map which provides a reference from a struct + to a key (or list of keys) on that struct which will be used as the primary key + (simple or composite) for diffing. """ @type diff_opts :: [ overrides: overrides ] @typedoc """ - A keyword list or a map which specifies a override from an Ecto schema to the desired + A keyword list or a map which specifies an override from an Ecto schema to the desired primary key, for use in comparing structs. Structs that are not specified will be compared using their default primary key. From f9c8b41776f3c1bbeb054c04ad6435e3b47519b3 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Tue, 29 Mar 2022 22:09:32 -0700 Subject: [PATCH 15/16] ecto version req should be ~> 3.0 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index da2b86b..8b7693d 100644 --- a/mix.exs +++ b/mix.exs @@ -83,7 +83,7 @@ defmodule EctoDiff.MixProject do {:credo, "~> 1.0", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, {:ecto_sql, "~> 3.7", only: [:dev, :test]}, - {:ecto, "~> 3.7"}, + {:ecto, "~> 3.0"}, {:ex_doc, "~> 0.28", only: :dev, runtime: false}, {:excoveralls, "~> 0.14", only: :test}, {:jason, ">= 1.0.0", only: [:dev, :test]}, From 23716916a4dddc560ca22619a1bb4612853bab65 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg <42327429+vanvoljg@users.noreply.github.com> Date: Wed, 6 Apr 2022 14:22:54 -0700 Subject: [PATCH 16/16] update override typespec --- lib/ecto_diff.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ecto_diff.ex b/lib/ecto_diff.ex index 3f06d96..e0097b9 100644 --- a/lib/ecto_diff.ex +++ b/lib/ecto_diff.ex @@ -79,7 +79,7 @@ defmodule EctoDiff do %{Skill => :refid, Owner => [:id, :refid]} """ - @type overrides :: [{Ecto.Schema.t(), primary_key}] | %{Ecto.Schema.t() => primary_key} + @type overrides :: [{module, primary_key}] | %{module => primary_key} @typedoc """ A struct field or list of fields used to define a simple or composite primary key.