In my app I have this resource (some parts omitted):
defmodule MyApp.Vote do
use Ash.Resource,
data_layer: AshPostgres.DataLayer
actions do
create :cast do
argument :poll_id, :uuid, allow_nil?: false
argument :option_id, :uuid, allow_nil?: false
upsert? true
upsert_identity :unique_voter_per_poll
change manage_relationship(:poll_id, :poll, type: :append_and_remove)
change manage_relationship(:option_id, :option, type: :append_and_remove)
change relate_actor(:voter)
end
end
relationships do
belongs_to :poll, Poll do
primary_key? true
allow_nil? false
end
belongs_to :option, Option do
primary_key? true
allow_nil? false
end
belongs_to :voter, Voter do
primary_key? true
allow_nil? false
end
end
identities do
identity :unique_voter_per_poll, [:voter_id, :poll_id]
end
code_interface do
define_for MyApp.Voting
define :cast, args: [:poll_id, :option_id]
end
end
If a voter casts their vote for another option in the same poll, this is an upsert so the new option id overwrites the old one.
Now, I want to publish updates on a pubsub because I want to display (and update) the count of votes for each option in a Live View. This means that when I cast a vote, I want to receive a vote event for the option_id, but I also want to receive an unvote event for the previousoption_id.
I think what you’d need to do is handle that yourself with a custom notifier. You can write a notifier as a module with a notify/1 function and then reference it in your resource:
resource do
simple_notifiers [YourNotifier]
end
Then you can publish a message for vote and unvote respectively. Although, thinking about it, there isn’t a good way to select old attributes when doing an upsert right now. You may need to implement a manual action and use ecto directly to effectively handle this case.
I think I’ve found a workaround: I’ve added a custom change that reads the current option.
defmodule Tabemono.Voting.Vote.Changes.StorePreviousOption do
use Ash.Resource.Change
require Ash.Query
def change(changeset, _opts, ctx) do
poll_id = Ash.Changeset.get_argument(changeset, :poll_id)
voter_id = ctx.actor.id
previous_vote =
Tabemono.Voting.Vote
|> Ash.Query.lock("FOR UPDATE NOWAIT")
|> Ash.Query.filter(poll_id: poll_id, voter_id: voter_id)
|> Tabemono.Voting.read_one!()
if previous_vote do
Ash.Changeset.put_context(changeset, :old_option_id, previous_vote.option_id)
else
changeset
end
end
end
To be extra careful, I’ve added a row lock. This way, in the (unlikely) event that the same user tries to update the vote from two different locations, one of them should fail, ensuring a single notification is sent for that old option id.
After that, it’s just a matter of reading the changeset context in the notification that gets sent. I would’ve preferred to put this information in the notification metadata instead, but I’m not sure how to control it.
Is this solutions reasonable or does it have any problem?