Skip to content

Commit

Permalink
improve handling of relation fields that are not preloaded
Browse files Browse the repository at this point in the history
  • Loading branch information
woylie committed Aug 30, 2021
1 parent eddbcc6 commit 02d56cb
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 11 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

## [0.1.3] - 2021-08-30

### Changed

- Raise `EctoNestedChangeset.NotLoadedError` in case the relation field of a
loaded resource is not preloaded.
- Handle list operations on root level relation fields if the field is not
preloaded and the data is not persisted.

## [0.1.2] - 2021-08-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Add `ecto_nested_changeset` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ecto_nested_changeset, "~> 0.1.2"}
{:ecto_nested_changeset, "~> 0.1.3"}
]
end
```
Expand Down
33 changes: 24 additions & 9 deletions lib/ecto_nested_changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,14 @@ defmodule EctoNestedChangeset do
defp nested_update(:append, %Changeset{} = changeset, [field], value)
when is_atom(field) do
new_value =
case {get_change_or_field(changeset, field), changeset.action} do
{%NotLoaded{}, :insert} -> [value]
{previous_value, _} -> previous_value ++ [value]
case get_change_or_field(changeset, field) do
%NotLoaded{} ->
if Ecto.get_meta(changeset.data, :state) == :built,
do: [value],
else: raise(EctoNestedChangeset.NotLoadedError, field: field)

previous_value ->
previous_value ++ [value]
end

Changeset.put_change(changeset, field, new_value)
Expand All @@ -236,9 +241,14 @@ defmodule EctoNestedChangeset do
defp nested_update(:prepend, %Changeset{} = changeset, [field], value)
when is_atom(field) do
new_value =
case {get_change_or_field(changeset, field), changeset.action} do
{%NotLoaded{}, :insert} -> [value]
{previous_value, _} -> [value | previous_value]
case get_change_or_field(changeset, field) do
%NotLoaded{} ->
if Ecto.get_meta(changeset.data, :state) == :built,
do: [value],
else: raise(EctoNestedChangeset.NotLoadedError, field: field)

previous_value ->
[value | previous_value]
end

Changeset.put_change(changeset, field, new_value)
Expand All @@ -259,9 +269,14 @@ defmodule EctoNestedChangeset do
defp nested_update(:insert, %Changeset{} = changeset, [field, index], value)
when is_atom(field) and is_integer(index) do
new_value =
case {get_change_or_field(changeset, field), changeset.action} do
{%NotLoaded{}, :insert} -> [value]
{previous_value, _} -> List.insert_at(previous_value, index, value)
case get_change_or_field(changeset, field) do
%NotLoaded{} ->
if Ecto.get_meta(changeset.data, :state) == :built,
do: [value],
else: raise(EctoNestedChangeset.NotLoadedError, field: field)

previous_value ->
List.insert_at(previous_value, index, value)
end

Changeset.put_change(changeset, field, new_value)
Expand Down
12 changes: 12 additions & 0 deletions lib/exceptions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule EctoNestedChangeset.NotLoadedError do
@moduledoc """
Raised when a relation field that is updated is not preloaded.
"""
defexception [:field, :message]

def exception(opts) do
field = Keyword.fetch!(opts, :field)
message = "field `#{inspect(field)}` is not loaded"
%__MODULE__{field: field, message: message}
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule EctoNestedChangeset.MixProject do
use Mix.Project

@version "0.1.2"
@version "0.1.3"
@source_url "https://github.com/woylie/ecto_nested_changeset"

def project do
Expand Down
51 changes: 51 additions & 0 deletions test/ecto_nested_changeset_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ defmodule EctoNestedChangesetTest do
} = changeset.changes
end

test "doesn't raise error if field of unpersisted resource is not loaded" do
%Category{id: 1}
|> change()
|> append_at(:posts, %Post{title: "first"})
end

test "raises error if field of persisted resource is not preloaded" do
assert_raise EctoNestedChangeset.NotLoadedError,
"field `:posts` is not loaded",
fn ->
%Category{id: 1}
|> Map.update!(:__meta__, &Map.put(&1, :state, :loaded))
|> change()
|> append_at(:posts, %Post{title: "first"})
end
end

test "appends item at a sub field of a new list item" do
changeset =
%Category{id: 1, posts: []}
Expand Down Expand Up @@ -189,6 +206,23 @@ defmodule EctoNestedChangesetTest do
} = changeset.changes
end

test "doesn't raise error if field of unpersisted resource is not loaded" do
%Category{id: 1}
|> change()
|> prepend_at(:posts, %Post{title: "first"})
end

test "raises error if field of persisted resource is not preloaded" do
assert_raise EctoNestedChangeset.NotLoadedError,
"field `:posts` is not loaded",
fn ->
%Category{id: 1}
|> Map.update!(:__meta__, &Map.put(&1, :state, :loaded))
|> change()
|> prepend_at(:posts, %Post{title: "first"})
end
end

test "prepends item at a sub field of a new list item" do
changeset =
%Category{id: 1, posts: []}
Expand Down Expand Up @@ -333,6 +367,23 @@ defmodule EctoNestedChangesetTest do
} = changeset.changes
end

test "doesn't raise error if field of unpersisted resource is not loaded" do
%Category{id: 1}
|> change()
|> insert_at([:posts, 0], %Post{title: "first"})
end

test "raises error if field of persisted resource is not preloaded" do
assert_raise EctoNestedChangeset.NotLoadedError,
"field `:posts` is not loaded",
fn ->
%Category{id: 1}
|> Map.update!(:__meta__, &Map.put(&1, :state, :loaded))
|> change()
|> insert_at([:posts, 0], %Post{title: "first"})
end
end

test "inserts item at a sub field of a new list item" do
changeset =
%Category{id: 1, posts: []}
Expand Down

0 comments on commit 02d56cb

Please sign in to comment.