Transaction / Multi and changeset crashing silently when adding a param (validate_required not passing)

I’ve got this mutation going through Absinthe

mutation signup(
  $email: String!
  $firstName: String!
  $lastName: String!
  $organizationInput: OrganizationInput!
  $password: String!
) {
  signup(
    email: $email
    firstName: $firstName
    lastName: $lastName
    organization: $organizationInput
    password: $password
  ) {
    id
  }
}

What’s important is inside $organizationInput which is basically an input with 2 fields name and slug (name being required and slug optional)

The execution happens in this code

resolve fn _parent, args, _resolution ->
  Bigseat.Dashboard.People.create(args)
end

Which is

  def create(params = %{ organization: organization_params } \\ %{}) do
    organization_changeset = %Organization{slug: Slug.slugify(organization_params.name)}
    |> Organization.changeset(organization_params)

    multi = Multi.new
    |> Multi.insert(:organization, organization_changeset)
    |> Multi.run(:person, fn _repo, %{organization: organization} ->
      %Person{}
      |> Person.changeset(params)
      |> Ecto.Changeset.put_assoc(:organization, organization)
      |> Repo.insert()
    end)

    case Repo.transaction(multi) do
      {:ok, %{person: person}} -> {:ok, person}
      {:error, _model, changeset, _changes_so_far} -> {:error, changeset}
    end
  end

Basically, I’ve got a transaction (multi) which contains a Person and Organization schema and when you’ll be creating a Person you’ll also create an Organization which needs a slug and name

When transmitting slug through the input it processes perfectly and works. But when I don’t transmit it make it via %Organization{slug: Slug.slugify(organization_params.name)} the transaction just breaks silently without emitting error. I’ve also tried Map.merge and checked Slug.slugify and replaced it with a manual string I wrote, the end result is the same: it breaks.

The weirdest thing is it tells me the Organization schema is valid, it’s the Person one which breaks

[debug] QUERY OK db=0.7ms
rollback []
{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{
     email: "test@test.com",
     first_name: "Laurent",
     last_name: "Something",
     organization: #Ecto.Changeset<action: :update, changes: %{}, errors: [],
      data: #Bigseat.Dashboard.Organization<>, valid?: true>
   },
   errors: [],
   data: #Bigseat.Dashboard.Person<>,
   valid?: false
 >}

The fact there’s no errors ([]) will output a false positive on GraphQL (no error but a null id), I’ve no detail on why it shows valid?: false

I’ve also compared both way of doing it and they are the same

pry(1)> IEx.Info.info(organization_params)
[{"Data type", "Map"}, {"Reference modules", "Map"}]
pry(2)> IEx.Info.info(Map.merge(%{slug: Slug.slugify(organization_params.name)}, organization_params))
[{"Data type", "Map"}, {"Reference modules", "Map"}]

I’m fairly new to the Elixir world, but what could be the problem here? Why having oganization_params = %{name: "Hello", slug: "hello"} rather than %{ organization: organization_params } given from the arguments of the method would change anything?

Those other examples which are either working or not disturb me:

# not working
organization_changeset = Organization.changeset(%Organization{}, %{name: "test", slug: "hello"})
# working
organization_changeset = Organization.changeset(%Organization{}, organization_params)
# organization_params contains %{name: "test", slug: "hello"} and was transmitted through the resolver

See below the changeset of both Person and Organization if that helps digging down the problem.

# organization.ex
  def changeset(organization, attrs) do
    organization
    |> cast(attrs, [:slug, :name])
    |> cast_assoc(:people)
    |> validate_required([:slug, :name])
    |> unique_constraint(:slug)
  end

# person.ex
  def changeset(person, attrs) do
    person
    |> cast(attrs, [:email, :encrypted_password, :first_name, :last_name, :is_admin, :group])
    |> cast_assoc(:organization)
    |> validate_required([:email, :first_name, :last_name])
  end

After investigating for a while, I’ve noticed that removing the

# organization.ex
|> validate_required([:slug, :name])

Fixed the issue. I’ve a few problem regarding this:

  1. What does that even mean? The slug is required indeed and what’s given to the changeset is the exact same thing, why would it work or not?
  2. It means the Organization validation doesn’t pass, why is it not showing it? Why is the Person not passing?
  3. Why is there no trace of the error?

I’m extremely confused right now.

organization_changeset = %Organization{slug: Slug.slugify(organization_params.name)}
 |> Organization.changeset(organization_params)

This should probably be:

organization_changeset = %Organization{} 
  |> Organization.changeset(Map.put(organization_params, slug: Slug.slugify(organization_params.name))

changeset looks for what is different between the input (the initial Organization struct) and the params that are passed in. So if no slug is passed in to the second parameter of changeset, no slug gets added! Why?

Well, changeset(organization, changes) sees the slug in the organization already, but none in the changes map, so doesn’t schedule a change … we can see this here:

#Ecto.Changeset<action: :update, changes: %{}, errors: [],
  data: #Bigseat.Dashboard.Organization<>, valid?: true>

No changes, so it is valid … but also no data associated with it.

You then pass that changeset into put_assoc and it is now lacking the slug., and so fails when the Person changeset tries to build a query for the association as it does not have the slug in there.

When you pass in the slug with the parameters, it is passed as the args (2nd parameter) to changeset, and so is present in the changeset passed to the Person changeset, and so that passes the validate_required clause.

I’m wondering if some of your other copy’n’paste code is accurate though? Like:

organization_changeset = Organization.changeset(%Organization{}, %{name: "test", slug: "hello"})

is not what you have in the create function.

Hey,

Thanks for answering, I’ve actually tried many things before giving those examples, and well I had put what you said also without success. I’m lost when it comes to

organization_changeset = Organization.changeset(%Organization{}, %{name: "test", slug: "hello"})

Not working if I have

# organization.ex
|> validate_required([:slug, :name])

Those were other tests I’ve made but changed in my example

It’ll work only if i’m transmitting it from the GraphQL through params, is that normal behavior? Also, if I remove this validation it does insert everything correctly which means the slug was always present and will be inserted, but this validation doesn’t pass…

To give you another example

organization_changeset = Organization.changeset(%Organization{}, Map.merge(%{slug: slug}, organization_params))

This returns

#Ecto.Changeset<
  action: nil,
  changes: %{name: "BigSeatddkkk", slug: "bigseatddkkk"},
  errors: [],
  data: #Bigseat.Dashboard.Organization<>,
  valid?: true

But it won’t work anyway, as I can see with the rollback here

[debug] QUERY OK db=0.2ms idle=857.3ms
begin []
[debug] QUERY OK db=2.1ms
INSERT INTO "organizations" ("name","slug","inserted_at","updated_at","id") VALUES ($1,$2,$3,$4,$5) ["BigSeatddkkk", "bigseatddkkk", ~N[2021-02-08 01:21:08], ~N[2021-02-08 01:21:08], <<145, 70, 86, 90, 211, 52, 74, 206, 166, 210, 103, 61, 228, 196, 3, 135>>]
[debug] QUERY OK db=0.2ms
rollback []

And when highlighting the People#create it returns this

{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{
     email: "test@test.com",
     first_name: "Laurent",
     last_name: "Something",
     organization: #Ecto.Changeset<action: :update, changes: %{}, errors: [],
      data: #Bigseat.Dashboard.Organization<>, valid?: true>
   },
   errors: [],
   data: #Bigseat.Dashboard.Person<>,
   valid?: false
 >}

What’s interesting is the changes is empty, but before the Multi it was filled with the slug and name, so maybe it has to do with the transaction mechanism and Ecto?

multi = Multi.new
|> Multi.insert(:organization, organization_changeset)
|> Multi.run(:person, fn _repo, %{organization: organization} ->

[...]

I’ve finally found out what was wrong. When transmitting params I was also transmitting params.organization which had no slug so it broke the validation, but since we were in a multi it was also overwriting this problem by considering Organization valid.

In short, I was inserting wrong params into Person and because I was doing a cast_assoc and allowing the organization parameters it was not passing, but stayed silent about why.

I’ve fixed it, but I think there’s an output problem here when using multi, it should be more clear. Thanks @aseigo for trying to help me anyway.