Self-follow check used get_attribute(:follower_id) which is nil at validation time because relate_actor runs after validations in Ash. Fixed to use context.actor.id directly. Added 9 tests covering: follow, follow idempotency, self-follow prevention, guest restriction, unfollow, unfollow noop, guest unfollow, follower/following counts, and am_i_following. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
105 lines
2.2 KiB
Elixir
105 lines
2.2 KiB
Elixir
defmodule Mixer.Accounts.Follow do
|
|
require Ash.Query
|
|
|
|
use Ash.Resource,
|
|
domain: Mixer.Accounts,
|
|
data_layer: AshPostgres.DataLayer,
|
|
authorizers: [Ash.Policy.Authorizer],
|
|
extensions: [AshTypescript.Resource]
|
|
|
|
postgres do
|
|
table "follows"
|
|
repo Mixer.Repo
|
|
|
|
references do
|
|
reference :follower, on_delete: :delete
|
|
reference :following, on_delete: :delete
|
|
end
|
|
end
|
|
|
|
typescript do
|
|
type_name "follows"
|
|
end
|
|
|
|
actions do
|
|
defaults [:read, :destroy]
|
|
|
|
create :follow do
|
|
primary? true
|
|
upsert? true
|
|
upsert_identity :unique_follow
|
|
accept [:following_id]
|
|
change relate_actor(:follower)
|
|
|
|
validate fn changeset, context ->
|
|
actor_id = context.actor && context.actor.id
|
|
following_id = Ash.Changeset.get_attribute(changeset, :following_id)
|
|
|
|
if actor_id && actor_id == following_id do
|
|
{:error, field: :following_id, message: "You cannot follow yourself"}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
end
|
|
|
|
action :unfollow do
|
|
argument :following_id, :uuid, allow_nil?: false
|
|
|
|
run fn input, context ->
|
|
actor = context.actor
|
|
|
|
Mixer.Accounts.Follow
|
|
|> Ash.Query.filter(
|
|
Ash.Expr.expr(
|
|
follower_id == ^actor.id and following_id == ^input.arguments.following_id
|
|
)
|
|
)
|
|
|> Ash.read_one(authorize?: false)
|
|
|> case do
|
|
{:ok, nil} -> :ok
|
|
{:ok, follow} -> Ash.destroy(follow, authorize?: false)
|
|
{:error, error} -> {:error, error}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
policies do
|
|
policy action_type(:read) do
|
|
authorize_if always()
|
|
end
|
|
|
|
policy action(:follow) do
|
|
authorize_if actor_present()
|
|
end
|
|
|
|
policy action(:unfollow) do
|
|
authorize_if actor_present()
|
|
end
|
|
end
|
|
|
|
attributes do
|
|
uuid_primary_key :id
|
|
create_timestamp :created_at
|
|
end
|
|
|
|
relationships do
|
|
belongs_to :follower, Mixer.Accounts.User do
|
|
primary_key? true
|
|
allow_nil? false
|
|
attribute_writable? true
|
|
end
|
|
|
|
belongs_to :following, Mixer.Accounts.User do
|
|
primary_key? true
|
|
allow_nil? false
|
|
attribute_writable? true
|
|
end
|
|
end
|
|
|
|
identities do
|
|
identity :unique_follow, [:follower_id, :following_id]
|
|
end
|
|
end
|