cast_assoc doesn’t seem to work when I create or update with an existing item in the many_to_many relationship and the item has a required field. See below for example:
defmodule EctoTest.Repo.Migrations.AddTodoListsAndItems do
use Ecto.Migration
def change do
create table("todo_lists") do
add(:title, :string, null: false)
timestamps()
end
create table("todo_items") do
add(:description, :string, null: false)
timestamps()
end
create table("todo_lists_items", primary_key: false) do
add(:todo_item_id, references(:todo_items), null: true)
add(:todo_list_id, references(:todo_lists), null: true)
end
end
end
defmodule EctoTest.Todos.TodoList do
use Ecto.Schema
alias Ecto.Changeset
alias EctoTest.Todos.TodoItem
schema "todo_lists" do
field :title
many_to_many :todo_items, TodoItem, join_through: "todo_lists_items", on_replace: :delete
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> Changeset.cast(params, [:title])
|> Changeset.cast_assoc(:todo_items, required: true)
end
end
defmodule EctoTest.Todos.TodoItem do
use Ecto.Schema
alias Ecto.Changeset
schema "todo_items" do
field :description
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> Changeset.cast(params, [:description])
|> Changeset.validate_required([:description])
end
end
defmodule EctoTest.Todos do
alias EctoTest.Repo
alias EctoTest.Todos.TodoItem
alias EctoTest.Todos.TodoList
def create_todo_item(attrs \\ %{}) do
%TodoItem{}
|> TodoItem.changeset(attrs)
|> Repo.insert()
end
def create_todo_list(attrs \\ %{}) do
%TodoList{}
|> TodoList.changeset(attrs)
|> Repo.insert()
end
def update_todo_list(%TodoList{} = todo_list, attrs \\ %{}) do
todo_list
|> Repo.preload([:todo_items])
|> TodoList.changeset(attrs)
|> Repo.update()
end
end
Both the following tests fail with similar errors
defmodule EctoTest.Todos.TodoListTest do
use EctoTest.DataCase
alias EctoTest.Todos
describe "create_todo_list/2" do
test "creates todo_list and casts todo_items" do
{:ok, todo_item} = Todos.create_todo_item(%{description: "Test description"})
todo_list_params = %{title: "Test title", todo_items: [%{id: todo_item.id}]}
assert {:ok, todo_list} = Todos.create_todo_list(todo_list_params)
end
end
describe "update_todo_list/2" do
test "updates todo_list and casts todo_items" do
{:ok, todo_item} = Todos.create_todo_item(%{description: "Test description"})
todo_list_params = %{title: "Test title", todo_items: [%{id: todo_item.id}]}
assert {:ok, todo_list} = Todos.create_todo_list(todo_list_params)
{:ok, new_todo_item} = Todos.create_todo_item(%{description: "New todo item"})
todo_list_params = %{todo_items: [%{id: new_todo_item.id}]}
assert {:ok, todo_list} = Todos.update_todo_list(todo_list, todo_list_params)
end
end
end
These are the errors I get
1) test update_todo_list/2 updates todo_list and casts todo_items (EctoTest.Todos.TodoListTest)
test/ecto_test/todos/todo_list_test.exs:15
match (=) failed
code: assert {:ok, todo_list} = Todos.create_todo_list(todo_list_params)
right: {:error,
#Ecto.Changeset<
action: :insert,
changes: %{
title: "Test title",
todo_items: [
#Ecto.Changeset<
action: :insert,
changes: %{},
errors: [
description: {"can't be blank", [validation: :required]}
],
data: #EctoTest.Todos.TodoItem<>,
valid?: false
>
]
},
errors: [],
data: #EctoTest.Todos.TodoList<>,
valid?: false
>}
stacktrace:
test/ecto_test/todos/todo_list_test.exs:19: (test)
2) test create_todo_list/2 creates todo_list and casts todo_items (EctoTest.Todos.TodoListTest)
test/ecto_test/todos/todo_list_test.exs:6
match (=) failed
code: assert {:ok, todo_list} = Todos.create_todo_list(todo_list_params)
right: {:error,
#Ecto.Changeset<
action: :insert,
changes: %{
title: "Test title",
todo_items: [
#Ecto.Changeset<
action: :insert,
changes: %{},
errors: [
description: {"can't be blank", [validation: :required]}
],
data: #EctoTest.Todos.TodoItem<>,
valid?: false
>
]
},
errors: [],
data: #EctoTest.Todos.TodoList<>,
valid?: false
>}
stacktrace:
test/ecto_test/todos/todo_list_test.exs:10: (test)
Is this expected behavior? If so, what’s the best way for me to make this work?