Best practice to implement dynamic filtering for a read action?
Eduardo B. Alexandre:
Let’s say I have a page that shows a bunch of products for sale.
Now I want to start giving the user options for him to filter some of the products, let’s say by department, by mix and/or max price, date, etc.
Not only that but I also want to being able to sort by different fields (ex: price, date).
I was wondering what is the best approach to accomplish that using Ash.
I think I could easily do something like run
for_read
and then do a bunch of
Ash.Query.filter
or
Ash.Query.sort
depending on what inputs the user sent.
But for me this doesn’t seem correct, not only I would be moving business logic to my live module (I’m using LiveView, not GraphQL or JsonAPI in this case) instead of having it all being handled directly in my resources, but I would also be “creating” SQL queries outside my resource, which I don’t like because I prefer to limit what the actions other domains can do in my database.
What would you suggest in this case? Is it possible to write an action that would allow me to compose the query depending on its inputs?
ZachDaniel:
<a:wavey:989613883197095946> there are a few ways you can approach this
ZachDaniel:
-
add arguments to your action, and switch on them in a preparation
read :front_page do argument :name, :string prepare fn query, _ -> case Ash.Changeset.fetch_argument(query, :name) do {:ok, name} -> Ash.Query.filter(query, name == name) _ -> query end end end
ZachDaniel:
You can add as many arguments and as many prepare statements as you want
Eduardo B. Alexandre:
Amazing!!
ZachDaniel:
-
filters support passing in simple maps/keyword lists of data. You could build one up based on inputs, and pass it in. You can use
Ash.Query.do_filter/2
which is not a macro in this case (but usingAsh.Query.filter/2
is also fine).filter = %{ name: "fred" # or name: [eq: "fred"] # or (this has a bug that I'm pushing a fix to main for) contains: {Ash.Filter.TemplateHelpers.ref(:name), "fred"} } Ash.Query.filter(resource, ^filter) # or Ash.Query.do_filter(resource, filter)
ZachDaniel:
-
There is a tool for this in
AshPhoenix
calledAshPhoenix.FilterForm
ZachDaniel:
It hasn’t been documented very well, but it can be used to build filters and then instead of “submitting” the form, you say
AshPhoenix.FilterForm.filter(query, form)
)
ZachDaniel:
And if you want it to filter on type you could say something like:
def handle_event("filter_change", %{"filter" => params}, socket) do
filter_form = AshPhoenix.FilterForm.validate(socket.assigns.filter_form, params)
if filter_form.valid? do
new_data =
socket.assigns.query
|> AshPhoenix.FilterForm.query(params)
|> MyApi.read!()
{:noreply, assign(socket, filter_form: filter_form, data: new_data}
else
{:noreply, assign(socket, filter_form: filter_form)}
end
end
arosenb2:
Can
validate
also be invoked inside this? I was looking at validating a field with a regex, but only if the field is present (i.e. not a required field)
ZachDaniel:
validations are not supported in read actions currently, but we could add something similar at some point.
ZachDaniel:
But you can do that validation in a custom preparation