Scenario
I created a Registration schema embedded_schema
that is tied to a form.
Accounts.Registration.register_user()
will validate the registration form which then converts
it to a Accounts.User.changeset()
and then does a Repo.insert()
wrapped in a Multi.new()
defmodule Try.Accounts.Registration do
# ...
@primary_key false
embedded_schema do
field :name, :string
field :email, :string
field :password, :string
end
def changeset(struct, params) do
struct
|> cast(params, [:password, :name, :email])
|> validate_required([:password, :name, :email])
|> validate_length(:password, min: 3, max: 10)
end
def register_user(params) do
chset = changeset(%Registration{}, params)
Multi.new()
|> Multi.run(:registration, &apply_registration(&1, chset))
|> Multi.run(:user, fn ops ->
Repo.insert(to_user_changeset(%User{}, ops.registration))
end)
|> Repo.transaction()
end
defp apply_registration(_changes, changeset) do
if changeset.valid? do
{:ok, apply_changes(changeset)}
else
{:error, changeset}
end
end
defp to_user_changeset(user, %Registration{} = reg) do
User.changeset(user, Map.take(reg, [:email, :name, :password]))
end
end
My Problem
In my RegisterController.create()
I will get an error on the unique_constraint(:email)
for the User
and what gets returned is a User.changeset()
and not a Registration.changeset()
. Which makes sense because that validation is tied to the User
schema.
By using Multi.new I get an error tuple that looks like this
{:error, failed_operation, failed_value, changes_so_far}
If you review the last error tuple {:error, :user, failed_value, changes_so_far}
you can pattern match and you will see what returned back from the DB. I commented it out
defmodule TryWeb.RegisterController do
#...
def create(conn, %{"registration" => registration_params}) do
case Registration.register_user(registration_params) do
{:ok, _user} ->
conn
|> put_flash(:info, "Welcome!")
|> redirect(to: admin_home_path(conn, :index))
{:error, :registration, failed_value, _changes_so_far} ->
changeset = %{failed_value | action: :insert}
render(conn, "new.html", changeset: changeset)
{:error, :user, failed_value, changes_so_far} ->
IO.inspect failed_value
#------------
# failed_value
#------------
# Ecto.Changeset<action: :insert, changes: %{email: "foo@gmail.com", name: "foo",
# password_hash: "$2b$12$aauR/LLVhrirvd4P61EyB.US3mUTWP3zVMGlHVMBRhYPXgPQte6Ca"},
# errors: [email: {"has already been taken", []}], data: #Try.Accounts.User<>,
# valid?: false>
#------------
IO.inspect changes_so_far
# -----------------
# changes_so_far
# -----------------
# %{registration: %Try.Accounts.Registration{email: "foo@gmail.com", name: "foo", password: "pass"}}
# -----------------
render(conn, "new.html", changeset: changeset)
end
end
end
My Working Solution
In order to get my form.html
to work correctly I need to create a customized changeset
for that error tuple on :user
. I have to create a Registration.changeset()
that has an :error
of email taken even though the Registration
embed_schema does not support that validation. And remove the :password_hash
because the Registration
module does not contain it either. This seems to be the only way to do it. See solution below:
{:error, :user, failed_value, changes_so_far} ->
changeset = %{ failed_value | data: Map.get(changes_so_far, :registration)}
changeset = Ecto.Changeset.delete_change(changeset, :password_hash)
# -----------------
# Merging failed_value changeset and changes_so_far into a Registration
# -----------------
IO.inspect changeset
# Ecto.Changeset<action: :insert, changes: %{email: "foo@gmail.com", name: "foo"},
# errors: [email: {"has already been taken", []}], data: #Try.Accounts.Registration<>, valid?: false>
render(conn, "new.html", changeset: changeset)
Looking for Feedback
My solution works. But something tells me its not the right way of doing things. I’m returning a Registration.changeset with validation errors and attributes that do not belong to it but it’s the only way I can get the form.html to show the errors and work correctly. I hope I’m making sense here. Looking for feedback.