Best practice to implement dynamic filtering for a read action?

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?
5 Replies
ZachDaniel
ZachDaniel3y ago
:wavey: there are a few ways you can approach this 1. 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
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
You can add as many arguments and as many prepare statements as you want
Blibs
BlibsOP3y ago
Amazing!!
ZachDaniel
ZachDaniel3y ago
2. 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 using Ash.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)
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)
3. There is a tool for this in AshPhoenix called AshPhoenix.FilterForm 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)) 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
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
a-a-ron
a-a-ron3y ago
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
ZachDaniel3y ago
validations are not supported in read actions currently, but we could add something similar at some point. But you can do that validation in a custom preparation

Did you find this page helpful?