Files
Mixer/lib/mixer/posts/tweet.ex

298 lines
6.5 KiB
Elixir

defmodule Mixer.Posts.Tweet do
import Ash.Expr
require Ash.Query
use Ash.Resource,
otp_app: :mixer,
domain: Mixer.Posts,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshStateMachine, AshTypescript.Resource]
postgres do
table "tweets"
repo Mixer.Repo
references do
reference :parent_tweet, on_delete: :delete
end
end
state_machine do
initial_states [:drafted, :posted]
default_initial_state :drafted
transitions do
transition :create, from: :*, to: :posted
end
end
typescript do
type_name "tweets"
end
actions do
defaults [:read, :destroy]
read :following_feed do
filter expr(
user_id == ^actor(:id) or
exists(user.followers, follower_id == ^actor(:id))
)
end
create :create do
upsert? true
accept [:content, :parent_tweet_id]
argument :media_id, :uuid, allow_nil?: true
change relate_actor(:user)
change transition_state(:posted)
change fn changeset, context ->
case Ash.Changeset.get_argument(changeset, :media_id) do
nil ->
changeset
media_id ->
Ash.Changeset.after_action(changeset, fn _changeset, tweet ->
Mixer.Posts.Media
|> Ash.get!(media_id, authorize?: false)
|> Ash.Changeset.for_update(:link_to_tweet, %{tweet_id: tweet.id},
actor: context.actor
)
|> Ash.update!()
{:ok, tweet}
end)
end
end
end
update :update do
accept [:content]
end
update :like do
accept []
require_atomic? false
change fn changeset, context ->
Ash.Changeset.after_action(changeset, fn _changeset, tweet ->
case ensure_like(tweet, context.actor) do
{:created, _like} ->
increment_likes(tweet, context.actor)
{:noop, _like} ->
{:ok, tweet}
{:error, error} ->
{:error, error}
end
end)
end
end
update :unlike do
accept []
require_atomic? false
change fn changeset, context ->
Ash.Changeset.after_action(changeset, fn _changeset, tweet ->
case remove_like(tweet, context.actor) do
{:deleted, _like} ->
decrement_likes(tweet, context.actor)
{:noop, _like} ->
{:ok, tweet}
{:error, error} ->
{:error, error}
end
end)
end
end
update :increment_likes do
accept []
require_atomic? false
change atomic_update(:likes, expr(likes + 1))
end
update :decrement_likes do
accept []
require_atomic? false
change atomic_update(:likes, expr(likes - 1))
end
end
policies do
policy action_type(:read) do
authorize_if always()
end
policy action_type(:create) do
authorize_if actor_present()
end
policy action(:update) do
authorize_if relates_to_actor_via(:user)
end
policy action(:destroy) do
authorize_if relates_to_actor_via(:user)
authorize_if relates_to_actor_via([:parent_tweet, :user])
end
policy action(:like) do
authorize_if actor_present()
end
policy action(:unlike) do
authorize_if actor_present()
end
end
attributes do
uuid_primary_key :id
attribute :content, :string do
allow_nil? false
public? true
end
attribute :likes, :integer do
allow_nil? false
default 0
public? true
end
attribute :user_id, :uuid do
allow_nil? false
public? true
end
create_timestamp :inserted_at do
public? true
end
update_timestamp :updated_at
end
relationships do
belongs_to :user, Mixer.Accounts.User do
attribute_type :uuid
attribute_writable? true
allow_nil? false
public? true
end
belongs_to :parent_tweet, Mixer.Posts.Tweet do
attribute_type :uuid
attribute_writable? true
allow_nil? true
public? true
end
has_many :comments, Mixer.Posts.Tweet do
destination_attribute :parent_tweet_id
public? true
end
has_many :media, Mixer.Posts.Media do
public? true
end
has_many :tweet_likes, Mixer.Posts.TweetLike
end
calculations do
calculate :user_email, :string, expr(user.email) do
public? true
end
end
aggregates do
count :comment_count, :comments do
public? true
end
exists :liked_by_me, :tweet_likes do
public? true
filter expr(user_id == ^actor(:id))
end
end
defp ensure_like(_tweet, nil), do: {:error, Ash.Error.Forbidden.exception([])}
defp ensure_like(tweet, actor) do
case get_like(tweet.id, actor.id) do
{:ok, nil} ->
case create_like(tweet.id, actor) do
{:ok, like} ->
{:created, like}
{:error, error} ->
case get_like(tweet.id, actor.id) do
{:ok, nil} ->
{:error, error}
{:ok, like} ->
{:noop, like}
{:error, error} ->
{:error, error}
end
end
{:ok, like} ->
{:noop, like}
{:error, error} ->
{:error, error}
end
end
defp remove_like(_tweet, nil), do: {:error, Ash.Error.Forbidden.exception([])}
defp remove_like(tweet, actor) do
case get_like(tweet.id, actor.id) do
{:ok, nil} ->
{:noop, nil}
{:ok, like} ->
case Ash.destroy(like, actor: actor) do
:ok -> {:deleted, like}
{:ok, _destroyed_like} -> {:deleted, like}
{:error, error} -> {:error, error}
end
{:error, error} ->
{:error, error}
end
end
defp create_like(tweet_id, actor) do
Mixer.Posts.TweetLike
|> Ash.Changeset.for_create(:create, %{tweet_id: tweet_id}, actor: actor)
|> Ash.create()
end
defp get_like(tweet_id, user_id) do
Mixer.Posts.TweetLike
|> Ash.Query.filter(expr(tweet_id == ^tweet_id and user_id == ^user_id))
|> Ash.read_one(authorize?: false)
end
defp increment_likes(tweet, actor) do
tweet
|> Ash.Changeset.for_update(:increment_likes, %{}, actor: actor)
|> Ash.update(authorize?: false)
end
defp decrement_likes(tweet, actor) do
tweet
|> Ash.Changeset.for_update(:decrement_likes, %{}, actor: actor)
|> Ash.update(authorize?: false)
end
end