Using manage_relationship to delete a related record

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?
6 Replies
moxley
moxleyOP2y ago
Here's the action definition:
update :delete_note do
argument :id, :string

change(&Member2Util.delete_note/2)
end
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
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
ZachDaniel2y ago
Something does seem strange there... I don't think type: :remove will do the thing as that will attempt to "unrelate" the two things You might try manage_relationship(...., on_match: {:destroy, :destroy_action_name})
moxley
moxleyOP2y ago
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!()
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. I set on_match: {:destroy, :destroy}. 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
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
But it still doesn't delete the related record. 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. 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]
]}
]
}
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
ZachDaniel2y ago
Hard to follow the specifics. If you could reproduce the behavior in a test that would help a lot!
moxley
moxleyOP2y ago
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 == []
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 == []
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
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
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 == []
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 == []
Okay, I think this has something to do with with converting between string and integer IDs 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. That was it. 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
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. 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!()
# 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
ZachDaniel2y ago
So what likely needs to happen is that we need to cast values to the proper type and use Ash.Type.equal? Can you open an issue for this? It should be a relatively mechanical change. 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

Did you find this page helpful?