Using manage_relationship to delete a related record

moxley7725
2023-07-19

moxley7725:

I have an action that accepts the ID of a related record. It should delete that record. However, when I tried using the type: :remove option, Ash responds with Invalid value provided for notes: changes would create a new related record. Is this the right approach? Or should I manually delete the record?

moxley7725:

Here’s the action definition:

    update :delete_note do
      argument :id, :string

      change(&Member2Util.delete_note/2)
    end

Here’s the referenced delete_note() function:

  def delete_note(changeset, _context) do
    note_id = changeset.arguments[:id]
    note_attrs = %{id: note_id}

    Changeset.manage_relationship(changeset, :notes, [note_attrs],
      type: :remove,
      on_lookup: :ignore
    )
  end

zachdaniel:

Something does seem strange there…

zachdaniel:

I don’t think type: :remove will do the thing as that will attempt to “unrelate” the two things

zachdaniel:

You might try manage_relationship(...., on_match: {:destroy, :destroy_action_name})

moxley7725:

This works:

      note_attrs = %{id: to_string(orig_note.id)}

      _updated_member2 =
        member2
        |> Ash.Changeset.for_update(:update, %{}, authorize?: false)
        |> Ash.Changeset.manage_relationship(:notes, [note_attrs], on_match: {:destroy, :destroy})
        |> GF.Ash.update!()

It successfully deletes the related record.

This is a Member has_many Notes relationship.

moxley7725:

I set on_match: {:destroy, :destroy} .

moxley7725:

I also did it to the original code:

  def delete_note(changeset, _context) do
    note_id = changeset.arguments[:id]
    note_attrs = %{id: note_id}

    Changeset.manage_relationship(changeset, :notes, [note_attrs], on_match: {:destroy, :destroy})
    |> dbg()
  end

moxley7725:

But it still doesn’t delete the related record.

moxley7725:

I ran an IO.inspect at the end of the function above, and it looks exactly like the IO.inspect I applied when calling manage_relationship directly.

moxley7725:

They both contain this piece of data:

  relationships: %{
    notes: [
      {[%{id: "446"}],
       [
         ignore?: false,
         on_missing: :ignore,
         on_lookup: :ignore,
         on_no_match: :ignore,
         eager_validate_with: false,
         authorize?: true,
         on_match: {:destroy, :destroy},
         meta: [inputs_was_list?: true]
       ]}
    ]
  }

zachdaniel:

Hard to follow the specifics. If you could reproduce the behavior in a test that would help a lot!

moxley7725:

Here’s the test:

      note_attrs = %{id: to_string(orig_note.id)}

      _updated_member2 =
        member2
        |> Ash.Changeset.for_update(:delete_note, note_attrs, actor: ctx.session_member)
        |> GF.Ash.update!()

      member =
        GF.Members.Member2
        |> GF.Ash.get!(member2.id, authorize?: false)
        |> GF.Ash.load!(:notes)

      # Fails. Note is not deleted.
      assert member.notes == []

moxley7725:

Here’s the action:

    update :delete_note do
      argument :id, :string

      change(fn changeset, _context ->
        note_attrs = changeset.arguments

        Changeset.manage_relationship(changeset, :notes, [note_attrs],
          on_match: {:destroy, :destroy}
        )
      end)
    end

moxley7725:

Calling manage_relationship doesn’t delete the note either:

      note_attrs = %{id: to_string(orig_note.id)}

      _updated_member2 =
        member2
        |> Ash.Changeset.for_update(:update, %{}, authorize?: false,
 actor: ctx.session_member)
        |> Ash.Changeset.manage_relationship(:notes, [note_attrs], on_match: {:destroy, :destroy})
        |> GF.Ash.update!()

      member =
        GF.Members.Member2
        |> GF.Ash.get!(member2.id, authorize?: false)
        |> GF.Ash.load!(:notes)

      # Fails. Note is not deleted.
      assert member.notes == []

moxley7725:

Okay, I think this has something to do with with converting between string and integer IDs

moxley7725:

In the last test I posted, if I change this line, note_attrs = %{id: to_string(orig_note.id)} to note_attrs = %{id: orig_note.id} , it passes.

moxley7725:

That was it.

moxley7725:

I modified my action to this:

    update :delete_note do
      argument :id, :string

      change(fn changeset, _context ->
        note_attrs = %{id: String.to_integer(changeset.arguments[:id])}

        Changeset.manage_relationship(changeset, :notes, [note_attrs],
          on_match: {:destroy, :destroy}
        )
      end)
    end

I cast the string :id argument to an integer before calling manage_relationship .

moxley7725:

However, there seems to be the opposite issue when calling manage_relationship with type: :append :

      # note_attrs = %{id: to_string(orig_note.id), body: "updated note"}
      note_attrs = %{id: orig_note.id, body: "updated note"}

      member
      |> Ash.Changeset.for_update(:update, %{}, actor: session_member)
      |> Ash.Changeset.manage_relationship(:notes, [note_attrs], type: :append)
      |> GF.Ash.update!()

Here, the note doesn’t get updated when note_attrs.id is an integer. The note only updates when it’s a string.

zachdaniel:

So what likely needs to happen is that we need to cast values to the proper type and use Ash.Type.equal?

zachdaniel:

Can you open an issue for this? It should be a relatively mechanical change.

zachdaniel:

I think people haven’t encountered this before due to most ash users using UUIDs. We should of course fix it just thinking about how it could possibly have been broken for so long