Hi I’m trying to create a simple action to recommend and unrecommend.
For now logic that a person can only recommend one post at a time is implemented using identities
.
thumb_up.ex
defmodule Dentallog.DentalLab.ThumbUp do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [
AshGraphql.Resource
]
graphql do
type :dental_lab_post_thumb_up
mutations do
create :thumb_up_dental_lab_post, :thumb_up
destroy :cancel_thumb_up_dental_lab_post, :cancel
end
end
actions do
defaults [:read]
create :thumb_up do
change relate_actor(:user)
end
destroy :cancel do
change relate_actor(:user)
end
end
attributes do
uuid_primary_key :id
create_timestamp :created_at
update_timestamp :updated_at
end
relationships do
belongs_to :user, Dentallog.Accounts.User do
api Dentallog.Accounts
end
belongs_to :dental_lab_post, Dentallog.DentalLab.DentalLabPost do
allow_nil? false
attribute_writable? true
end
end
postgres do
table "dental_lab_thumb_ups"
repo Dentallog.Repo
end
identities do
identity :unique_thumbup, [:dental_lab_post_id, :user_id] do
eager_check_with Dentallog.DentalLab
end
end
end
But I’m not sure what to do about the logic for canceling.
I guess I could use a changeset, but that’s a bit difficult.
Currently, the PK works this: if you enter an ID, it gets deleted.
But I’d like to get the input as the ID of the post and authenticate the user via relate_actor(:user)
. (post_id, user_id)
Is there a good way to do this?
Also, I’m wondering if you recommend using identities
or atomic to allow for guarantee one-time recommendations.
You can do that with bulk destroy actions. We will be working on guides that explain how to use bulk actions more than what exists currently. But you can do this:
read :my_thumb_ups do
filter expr(id == ^actor(:id))
end
ThumbUp
|> Ash.Query.for_read(:my_thumb_ups, %{}, actor: actor)
|> Ash.Query.filter(dental_lab_post.id == ^post_id)
|> ThumbUp.bulk_destroy!(:destroy)
You can put this behind a generic action:
action :remove, :atom do
constraints one_of: [:ok]
argument :post_id, :uuid, allow_nil?: false
run fn input, context ->
ThumbUp
|> Ash.Query.for_read(:my_thumb_ups, %{}, Ash.context_to_opts(context))
|> Ash.Query.filter(dental_lab_post.id == ^input.post_id)
|> ThumbUp.bulk_destroy!(:destroy)
{:ok, :ok}
end
end
And then you can put that in your code_interface:
code_interface do
define_for YourApi
define :remove, args: [:post_id]
end
Then you can call it:
ThumbUp.remove!(post.id)
1 Like
Thank you for your answer, Jack!
I think I’m getting better at ash thanks to you.
Sorry, this may be off topic, but I have one more question.
I would like to expose the action with AshGraphql.
Do you happen to know how to do that for actions?
I looked in the documentation and didn’t find it.
...
graphql do
type :dental_lab_post_thumb_up
actions do
action :cancel_thumb_up, :cancel
end
mutations do
create :thumb_up_dental_lab_post, :thumb_up
end
end
...
I’m not familiar with absinthe yet, and I was wondering if I should use schema.ex
to do this?
Generic actions are exposed as either mutations or queries. You ultimately decide which one it should be, in this case a mutation makes sense since it is making a change in the action.
graphql do
type :dental_lab_post_thumb_up
mutations do
create :thumb_up_dental_lab_post, :thumb_up
action :cancel_thumb_up, :cancel
end
end
On my phone , but IIRC that is how it’s done
When I do that, I got an error.
Do you happen to know the cause of the error?
** (Absinthe.Schema.Error) Compilation failed:
---------------------------------------
Location ## Location.
/users/dwlee/Documents/dentallog/dentallog-server/deps/ash_graphql/lib/resource/resource.ex:599
In the cancel_thumb-up field, :dental_lab_post_thumb_up_cancel is not defined in the schema.
If it is referenced, the type must exist.
(Absinthe 1.7.6) lib/absinthe/schema.ex:392: Absinthe.Schema.__after_compile__/2
(stdlib 5.2) lists.erl:1594: :lists.foldl/3
(Elixir 1.16.1) lib/kernel/parallel_compiler.ex:428: anonymous fn/5 in Kernel.ParallelCompiler.spawn_workers/8
This looks like a bug What happens if you define it in the query block instead? (thats not the answer, but will help me debug)
...
graphql do
type :dental_lab_post_thumb_up
queries do
action :cancel_thumb_up, :cancel
end
mutations do
create :thumb_up_dental_lab_post, :thumb_up
end
end
...
== Compilation error in file lib/dentallog/schema.ex ==
** (Absinthe.Schema.Error) Compilation failed:
---------------------------------------
## Locations
/Users/dwlee/Documents/dentallog/dentallog-server/deps/ash_graphql/lib/resource/resource.ex:524
In field Cancel_thumb_up, :dental_lab_post_thumb_up_cancel is not defined in your schema.
Types must exist if referenced.
(absinthe 1.7.6) lib/absinthe/schema.ex:392: Absinthe.Schema.__after_compile__/2
(stdlib 5.2) lists.erl:1594: :lists.foldl/3
I’m having the same issue
Ah, okay, I see the problem.
1 Like
I’ve fixed the issue in main, but I’d suggest changing your action in such a way that it will be better anyway, and will avoid the issue. If you make your action return the id of the post that was deleted
action :remove, :string do
argument :post_id, :uuid, allow_nil?: false
run fn input, context ->
ThumbUp
|> Ash.Query.for_read(:my_thumb_ups, %{}, Ash.context_to_opts(context))
|> Ash.Query.filter(dental_lab_post.id == ^input.post_id)
|> ThumbUp.bulk_destroy!(:destroy)
{:ok, input.post_id}
end
it should work
1 Like
To be clear, the issue is with using :atom
and constraints: :one_of
, so any other return type would avoid the issue (which should be fixed soon). You could even return the post that was destroyed.
action :remove, :struct do
constraints instance_of: __MODULE__
argument :post_id, :uuid, allow_nil?: false
run fn input, context ->
ThumbUp
|> Ash.Query.for_read(:my_thumb_ups, %{}, Ash.context_to_opts(context))
|> Ash.Query.filter(dental_lab_post.id == ^input.post_id)
|> ThumbUp.bulk_destroy(:destroy, return_records?: true, return_errors?: true)
|> case do
%Ash.BulkResult{status: :success, records: [destroyed]} ->
{:ok, destroyed}
%Ash.BulkResult{status: :success, records: []} ->
{:ok, nil}
%Ash.BulkResult{status: :error, errors: errors} ->
{:error, errors}
end
end
Also shown in this example is a way to manually return errors if desired, as opposed to raising.
1 Like
Thank you for your response. Jack!
It’s working fine.
graphql do
type :dental_lab_post_thumb_up
mutations do
create :thumb_up_dental_lab_post, :thumb_up
action :cancel_thumb_up, :cancel
end
end
...
read :my_thumb_ups do
filter expr(user_id == ^actor(:id))
end
action :cancel, :string do
argument :post_id, :uuid, allow_nil?: false
run fn input, context ->
Dentallog.DentalLab.ThumbUp
|> Ash.Query.for_read(:my_thumb_ups, %{}, Ash.context_to_opts(context))
|> Ash.Query.filter(dental_lab_post_id == ^input.arguments.post_id)
|> Dentallog.DentalLab.read!()
|> Dentallog.DentalLab.bulk_destroy!(:destroy,
return_records?: true,
return_errors?: true
)
{:ok, input.arguments.post_id}
end
end
end
1 Like
This is only tangentially related to your original question but I would recommend that you do in fact return the deleted post. This way you can automagically update your client cache (if you are using apollo as an example):
mutation myDeleteMutation($id: ID!) {
deleteSomething(id: $id) {
id
author {
id
posts {
id // <- this line here will tell apollo to remove the post from the list of posts in the authors cache
}
}
}
}
this way you dont have to do manual cache manipulations. Obviously this doesn’t really scale to thousands of rows. But for things where the total number of rows will always remain small this works really nicely.
2 Likes
Thanks for sharing good information
1 Like