We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Chapter 9: Following Your Favorite Artists (207)<br>
Okay so now we want to start to make sure that a person can follow an artist and using many-to-many relationships to do that. I believe this will involve some pubsub as well.<br>
Modelling with a Many-to-Many Relationship<br>
We can link users and users with a many-to-many relationship. A user can have many followed artists and the artist can have many ardent followers.<br>
Creating the ArtistFollower Resource<br>
We will create this with a join resource. This will sit between the two existing resource Tunez.Music.Artist and Tunez.Accounts.User. In this case we want it to follow the idea that it will go in the Music domain and be in the direction of user -> artist.<br>
mix ash.gen.resource Tunez.Music.ArtistFollower --extend postgres
With that done we can now head to lib/tunez/music/artist_follower.ex<br>
relationships do
belongs_to :artist, Tunez.Music.Artist do
primary_key? true
allow_nil? false
end
belongs_to :follower, Tunez.Accounts.User do
primary_key? true
allow_nil? false
end
end
So for our setup we will not have an id for them as there will be no way a user can follow an artist twice and as such the combination of user and artist will be enough to uniquely identify the relationship. Also we need to set up what happens when we delete an artist so that we don’t try and send out notifications to users about an artist that doesn’t exist.<br>
postgres do
table "artist_followers"
repo Tunez.Repo
references do
reference :artist, on_delete: :delete, index?: true
reference :follower, on_delete: :delete, index?: true
end
end
Now we can codegen and migrate the DB.<br>
mix ash.codegen create_artist_followers
mix ash.migrate
Using ArtistFollower to Link Artists and Users<br>
We want to be able to have this relationship go both ways and as such make sure that we can load all relevant data when we preload anything in the database. Let’s head to lib/tunez/music/artist.ex<br>
relationships do
has_many :follower_relationships, Tunez.Music.ArtistFollower
many_to_many :followers, Tunez.Accounts.User do
join_relationship :follower_relationships
destination_attribute_on_join_resource :follower_id
end
end
Now do the same for the user to denote the other direction. lib/tunez/accounts/user.ex<br>
relationships do
has_many :follower_relationships, Tunez.Music.ArtistFollower do
destination_attribute(:follower_id)
end
many_to_many :followed_artists, Tunez.Music.Artist do
join_relationship(:follower_relationships)
source_attribute_on_join_resource(:follower_id)
end
end
To use this we need a read action in artist_follower.ex<br>
use Ash.Resource,
otp_app: :tunez,
domain: Tunez.Music,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer]
actions do
defaults([:read])
end
policies do
policy action_type(:read) do
authorize_if(always())
end
end
Now we can test the resource in IEx.<br>
iex(8)> Tunez.Music.get_artist_by_id!(«uuid», load: [:follower_relationships])
#Tunez.Music.Artist<follower_relationships: [], ...>
Who Do You Follow?<br>
We need to make a way for a user to add an artist to follow. First we need to show if a user is already following that artist. lib/tunez/music/artist.ex<br>
calculations do
calculate(
:followed_by_me,
:boolean,
expr(exists(follower_relationships, follower_id == ^actor(:id)))
)
end
Showing the Current Following Status<br>
Load the data in the show page: lib/tunez_web/live/artists/show_live.ex<br>
def handle_params(%{"id" => artist_id}, _url, socket) do
artist =
Tunez.Music.get_artist_by_id!(artist_id,
load: [:followed_by_me, albums: [:duration, :tracks]],
actor: socket.assigns.current_user
)
end
Use a toggle in the HTML:<br>
<.h1>
{@artist.name}
<.follow_toggle on={@artist.followed_by_me} />
</.h1>
Following a New Artist<br>
Create a function to create a record in ArtistFollower. lib/tunez/music.ex<br>
resource Tunez.Music.ArtistFollower do
define :follow_artist, action: :create, args: [:artist]
end
Create the action in Tunez.Music.ArtistFollower<br>
mix ash.extend Tunez.Music.ArtistFollower graphql
lib/tunez/music.ex
resource Tunez.Music.ArtistFollower do
define :follow_artist do
action(:create)
args([:artist])
custom_input :artist, :struct do
constraints(instance_of: Tunez.Music.Artist)
transform(to: :artist_id, using: & &1.id)
end
end
end
lib/tunez/music/artist_follower.ex
actions do
defaults([:read])
create :create do
accept([:artist_id])
change(relate_actor(:follower, allow_nil?: false))
end
end
lib/tunez/music/artist_follower.ex
policies do
policy action_type(:read) do
authorize_if(always())
end
policy action_type(:create) do
authorize_if(actor_present())
end
end
Test in IEx:<br>
iex(5)> artist = Tunez.Music.get_artist_by_id!(«artist_uuid»)
iex(6)> user = Tunez.Accounts.get_user_by_email!(«email», authorize?: false)
iex(7)> Tunez.Music.follow_artist(artist, actor: user)
{:ok, %Tunez.Music.ArtistFollower{...}}
Add toggle in LiveView: lib/tunez_web/artists/show_live.ex<br>
def handle_event("follow", _params, socket) do
socket =
case Tunez.Music.follow_artist(socket.assigns.artist,
actor: socket.assigns.current_user
) do
{:ok, _} ->
update(socket, :artist, &%{&1 | followed_by_me: true})
{:error, _} ->
put_flash(socket, :error, "Could not follow artist")
end
{:noreply, socket}
end
Unfollowing an Old Artist<br>
Define action to destroy the follow relationship: lib/tunez/music.ex<br>
define :unfollow_artist do
action :destroy
args([:artist])
require_reference?(false)
custom_input :artist, :struct do
constraints(instance_of: Tunez.Music.Artist)
transform(to: :artist_id, using: & &1.id)
end
end
Be careful to only delete the right record:<br>
destroy :destroy do
argument :artist_id, :uuid do
allow_nil?(false)
end
change_filter(expr(artist_id == ^arg(:artist_id) && follower_id == ^actor(:id)))
end
Set policies in artist_follower.ex<br>
policy action_type(:destroy) do
authorize_if(actor_present())
end
Test:<br>
iex(8)> Tunez.Music.unfollow_artist!(artist, actor: user)
:ok
A Short Detour into Bulk Actions<br>
To avoid removing multiple rows unnecessarily, use the get_by option:<br>
lib/tunez/music.ex
define :unfollow_artist do
action(:destroy)
args([:artist])
get?(true)
require_reference?(false)
end
Integrate into LiveView: lib/tunez_web/artists/show_live.ex<br>
def handle_event("unfollow", _params, socket) do
socket =
case Tunez.Music.unfollow_artist(socket.assigns.artist,
actor: socket.assigns.current_user
) do
:ok ->
update(socket, :artist, &%{&1 | followed_by_me: false})
{:error, _} ->
put_flash(socket, :error, "Could not unfollow artist")
end
{:noreply, socket}
end
HTML toggle:<br>
<.h1>
{@artist.name}
<.follow_toggle :if={Tunez.Music.can_follow_artist?(@current_user, @artist)}
on={@artist.followed_by_me}
/>
</.h1>
Spicing Up the Artist Catalog<br>
Add more information on the index page to show follow status.
Showing the Follow Status for Each Artist<br>
Preload additional information in the search_artists resource: lib/tunez/music.ex<br>
define(:search_artists,
action: :search,
args: [:query],
default_options: fn ->
[
load: [
:followed_by_me,
:album_count,
:latest_album_year_released,
:cover_image_url
]
]
end
)
Index HTML: lib/tunez_web/artists/index_live.ex<br>
<.link navigate={~p"/artists/#{@artist.id}"}>
<.follow_icon :if={@artist.followed_by_me} />
<.cover_image image={@artist.cover_image_url} />
</.link>
Showing Follower Counts for Each Artist<br>
Add aggregates in lib/tunez/music/artist.ex<br>
aggregates do
count :album_count, :albums do
public?(true)
end
count :follower_count, :follower_relationships do
public?(true)
end
end
Load the new aggregate: lib/tunez/music.ex<br>
define(:search_artists,
action: :search,
args: [:query],
default_options: fn ->
[
load: [
:follower_count,
:followed_by_me,
:album_count,
:latest_album_year_released,
:cover_image_url
]
]
end
)
HTML:<br>
<p class="flex justify-between">
<.link
navigate={~p"/artists/#{@artist.id}"}
class="text-lg font-semibold"
data-role="artist-name"
>
{@artist.name}
</.link>
<.follower_count_display count={@artist.follower_count} />
</p>
Sorting Artists by Follower Status and Follower Count<br>
Add sorting options: lib/tunez_web/artists/index_live.ex<br>
defp sort_options do
[
{"recently updated", "-updated_at"},
{"recently added", "-inserted_at"},
{"name", "name"},
{"number of albums", "-album_count"},
{"latest album release", "-latest_album_year_released"},
{"popularity", "-follower_count"},
{"followed artists first", "-followed_by_me"}
]
end
Make calculations and aggregates public: lib/tunez/music/artist.ex<br>
calculations do
calculate(
:followed_by_me,
:boolean,
expr(exists(follower_relationships, follower_id == ^actor(:id))),
public?: true
)
end
aggregates do
count :album_count, :albums do
public?(true)
end
count :follower_count, :follower_relationships do
public?(true)
end
end
We now have the proper relationships and UI to follow and unfollow artists.<br>