-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Select fields option #154
Select fields option #154
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,9 +60,13 @@ defmodule EctoDiff do | |
* `: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. | ||
|
||
* `:select_fields` - A keyword list to select fields for diffing. | ||
This is useful when you only want to diff a subset of fields on a struct. | ||
""" | ||
@type diff_opts :: [ | ||
overrides: overrides | ||
overrides: overrides, | ||
select_fields: select_fields | ||
] | ||
|
||
@typedoc """ | ||
|
@@ -81,6 +85,26 @@ defmodule EctoDiff do | |
""" | ||
@type overrides :: [{module, primary_key}] | %{module => primary_key} | ||
|
||
@typedoc """ | ||
A keyword list specifying a subset of fields to diff on a struct, or associated struct. | ||
|
||
Fields that are not specified will not be diffed, or returned. | ||
|
||
Use `:all` to explicitly specify all fields. This is equivalent to not providing a `:select_fields` option. | ||
Use `[field: [:all]]` to specify all fields on a given struct. | ||
|
||
Selecting an association and keys without selecting that association field in the parent fields | ||
results in the associated struct being excluded, as it is not selected. | ||
|
||
Note on Embedded Schemas: fields within an embeds are all or nothing. You cannot select individual fields | ||
|
||
## Examples: | ||
|
||
[pet: [:name, :type, :skill], skill: [:all]] | ||
""" | ||
|
||
@type select_fields :: [{struct_field, :all} | {struct_field, [struct_field]}] | :all | ||
|
||
@typedoc """ | ||
A struct field or list of fields used to define a simple or composite primary key. | ||
""" | ||
|
@@ -248,7 +272,7 @@ defmodule EctoDiff do | |
defp do_diff(%struct{} = previous, %struct{} = current, opts) do | ||
primary_key_fields = get_primary_key_fields(struct, opts) | ||
|
||
field_changes = fields(previous, current) | ||
field_changes = fields(previous, current, opts) | ||
|
||
changes = | ||
field_changes | ||
|
@@ -276,10 +300,12 @@ defmodule EctoDiff do | |
} | ||
end | ||
|
||
defp fields(%struct{} = previous, %struct{} = current) do | ||
field_names = struct.__schema__(:fields) ++ (struct.__schema__(:virtual_fields) -- struct.__schema__(:embeds)) | ||
defp fields(%struct{} = previous, %struct{} = current, opts) do | ||
all_fields = struct.__schema__(:fields) ++ (struct.__schema__(:virtual_fields) -- struct.__schema__(:embeds)) | ||
|
||
field_names | ||
selected_fields = if_only_select_fields(all_fields, current, opts) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not a big fan of this PR, tbh. The point of EctoDiff is to show what changed between two structs. If the caller wants to ignore certain changes, they should be able to it after the diff has already been calculated. We could expose some helper functions that operate on an existing EctoDiff in order to do that. For example, we can define a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the reason that this is a non-default behavior that has to be explicitly selected, and configured, and that EctoDiff does not change.
There is no way to only select one of the |
||
|
||
selected_fields | ||
|> Enum.reduce([], &field(previous, current, &1, &2)) | ||
|> Map.new() | ||
end | ||
|
@@ -295,10 +321,34 @@ defmodule EctoDiff do | |
end | ||
end | ||
|
||
defp if_only_select_fields(fields, %struct{}, opts) do | ||
source = struct.__schema__(:source) | ||
select_fields = opts[:select_fields] | ||
|
||
if_only_select_fields(fields, source, select_fields) | ||
end | ||
|
||
defp if_only_select_fields(fields, source, select_fields) when is_nil(source) or is_nil(select_fields), | ||
do: fields | ||
|
||
defp if_only_select_fields(fields, _source, :all), do: fields | ||
|
||
defp if_only_select_fields(fields, source, select_fields) do | ||
key = String.to_atom(source) | ||
|
||
case select_fields[key] do | ||
[:all] -> fields | ||
[_ | _] = selected_fields -> Enum.filter(fields, &(&1 in selected_fields)) | ||
_ -> [] | ||
end | ||
end | ||
|
||
defp embeds(%struct{} = previous, %struct{} = current, opts) do | ||
embed_names = struct.__schema__(:embeds) | ||
|
||
embed_names | ||
selected_embeds = if_only_select_fields(embed_names, current, opts) | ||
|
||
selected_embeds | ||
|> Enum.reduce([], &embed(previous, current, &1, &2, opts)) | ||
|> Map.new() | ||
end | ||
|
@@ -319,7 +369,9 @@ defmodule EctoDiff do | |
defp associations(%struct{} = previous, %struct{} = current, opts) do | ||
association_names = struct.__schema__(:associations) | ||
|
||
association_names | ||
selected_associations = if_only_select_fields(association_names, current, opts) | ||
|
||
selected_associations | ||
|> Enum.reduce([], &association(previous, current, &1, &2, opts)) | ||
|> Map.new() | ||
end | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -187,6 +187,138 @@ defmodule EctoDiffTest do | |||||
} | ||||||
} = diff | ||||||
end | ||||||
|
||||||
test "can limit return to selected fields when select_fields specified in opts" do | ||||||
{:ok, pet} = | ||||||
%{ | ||||||
name: "Spot", | ||||||
skills: [%{name: "Eating"}, %{name: "Sleeping"}], | ||||||
owner: %{name: "Samuel"}, | ||||||
details: %{description: "It's a kitty!"}, | ||||||
resources: [%{toys: [%{name: "Ball", type: "Toy", quantity: 3}, %{name: "Bone", type: "Treat", quantity: 2}]}] | ||||||
} | ||||||
|> Pet.new() | ||||||
|> Repo.insert() | ||||||
|
||||||
pet = Repo.preload(pet, [:resources, :toys]) | ||||||
pet_id = pet.id | ||||||
details_id = pet.details.id | ||||||
[%{id: resource_id}] = pet.resources | ||||||
[ball_id, bone_id] = Enum.map(pet.toys, & &1.id) | ||||||
|
||||||
only_pet = [pets: [:name, :id]] | ||||||
{:ok, pet_diff} = EctoDiff.diff(nil, pet, select_fields: only_pet) | ||||||
assert %{id: {nil, pet_id}, name: {nil, "Spot"}} == pet_diff.changes | ||||||
|
||||||
# embeds are limited to the embeded field selection, not embeded fields selection | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ Typo:
Suggested change
|
||||||
# e.g. [pets: [:details]], [pets: [:details], details: [:all]], [pets: [:details], details: [:id]] | ||||||
# will all produce the same result | ||||||
only_details = [pets: [:details]] | ||||||
{:ok, details_diff} = EctoDiff.diff(nil, pet, select_fields: only_details) | ||||||
|
||||||
assert %{description: {nil, "It's a kitty!"}, id: {nil, details_id}} == details_diff.changes.details.changes | ||||||
assert Map.keys(details_diff.changes) == [:details] | ||||||
|
||||||
only_toys = [pets: [:toys], toys: [:all]] | ||||||
{:ok, toys_diff} = EctoDiff.diff(nil, pet, select_fields: only_toys) | ||||||
|
||||||
assert %{ | ||||||
toys: [ | ||||||
%EctoDiff{ | ||||||
changes: %{name: {nil, "Ball"}, quantity: {1, 3}, type: {nil, "Toy"}}, | ||||||
effect: :added, | ||||||
primary_key: %{id: ^ball_id} | ||||||
}, | ||||||
%EctoDiff{ | ||||||
changes: %{name: {nil, "Bone"}, quantity: {1, 2}, type: {nil, "Treat"}}, | ||||||
effect: :added, | ||||||
primary_key: %{id: ^bone_id} | ||||||
} | ||||||
] | ||||||
} = toys_diff.changes | ||||||
|
||||||
assert Map.keys(toys_diff.changes) == [:toys] | ||||||
|
||||||
only_pet_resources_and_toys = [ | ||||||
pets: [:name, :id, :resources], | ||||||
toys: [:name, :type, :quantity], | ||||||
resources: [:id, :toys] | ||||||
] | ||||||
|
||||||
{:ok, pet_resources_and_toys_diff} = EctoDiff.diff(nil, pet, select_fields: only_pet_resources_and_toys) | ||||||
|
||||||
assert %{ | ||||||
id: {nil, ^pet_id}, | ||||||
name: {nil, "Spot"}, | ||||||
resources: [ | ||||||
%EctoDiff{ | ||||||
changes: %{ | ||||||
id: {nil, ^resource_id}, | ||||||
toys: [ | ||||||
%EctoDiff{ | ||||||
changes: %{name: {nil, "Ball"}, quantity: {1, 3}, type: {nil, "Toy"}}, | ||||||
effect: :added, | ||||||
primary_key: %{id: ^ball_id} | ||||||
}, | ||||||
%EctoDiff{ | ||||||
changes: %{name: {nil, "Bone"}, quantity: {1, 2}, type: {nil, "Treat"}}, | ||||||
effect: :added, | ||||||
primary_key: %{id: ^bone_id} | ||||||
} | ||||||
] | ||||||
}, | ||||||
effect: :added, | ||||||
primary_key: %{id: ^resource_id} | ||||||
} | ||||||
] | ||||||
} = pet_resources_and_toys_diff.changes | ||||||
|
||||||
[resources] = pet_resources_and_toys_diff.changes.resources | ||||||
assert Map.keys(pet_resources_and_toys_diff.changes) == [:id, :name, :resources] | ||||||
assert Map.keys(resources.changes) == [:id, :toys] | ||||||
end | ||||||
|
||||||
test ":all arg in :select_fields returns all fields" do | ||||||
{:ok, pet} = | ||||||
%{ | ||||||
name: "Spot", | ||||||
skills: [%{name: "Eating"}, %{name: "Sleeping"}], | ||||||
owner: %{name: "Samuel"}, | ||||||
details: %{description: "It's a kitty!"}, | ||||||
resources: [%{toys: [%{name: "Ball", type: "Toy", quantity: 3}, %{name: "Bone", type: "Treat", quantity: 2}]}] | ||||||
} | ||||||
|> Pet.new() | ||||||
|> Repo.insert() | ||||||
|
||||||
pet = Repo.preload(pet, [:resources, :toys]) | ||||||
|
||||||
explicit_all = [pets: [:all], skills: [:all], owners: [:all], details: [:all], resources: [:all], toys: [:all]] | ||||||
implicit_all = :all | ||||||
{:ok, explicit_all_diff} = EctoDiff.diff(nil, pet, select_fields: explicit_all) | ||||||
{:ok, implicit_all_diff} = EctoDiff.diff(nil, pet, select_fields: implicit_all) | ||||||
{:ok, default_all_diff} = EctoDiff.diff(nil, pet) | ||||||
|
||||||
assert explicit_all_diff == implicit_all_diff | ||||||
assert explicit_all_diff == default_all_diff | ||||||
|
||||||
assert Map.keys(explicit_all_diff.changes) == [ | ||||||
:details, | ||||||
:id, | ||||||
:name, | ||||||
:owner, | ||||||
:owner_id, | ||||||
:refid, | ||||||
:resources, | ||||||
:skills, | ||||||
:toys | ||||||
] | ||||||
|
||||||
{:ok, pets_only_diff} = EctoDiff.diff(nil, pet, select_fields: [pets: [:all]]) | ||||||
assert Map.keys(pets_only_diff.changes) == [:details, :id, :name, :owner_id, :refid] | ||||||
|
||||||
{:ok, toys_only_diff} = EctoDiff.diff(nil, pet, select_fields: [pets: [:toys], toys: [:all]]) | ||||||
assert Map.keys(toys_only_diff.changes) == [:toys] | ||||||
end | ||||||
end | ||||||
|
||||||
describe "diff/2" do | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.