Argument in many to many filter

I have a 2 resources Client and Data in a many to many relationship through a join resource client_data. The join resource has 2 UUID columns for the clients and data, but it also has a 3rd boolean column exposed?. When reading clients through a read action, I want to provide an arg that says to load only the exposed related Data of a Client or the non exposed or both. So I've set an argument on the read action, but now how do I access it in the filter expr on the many to many relationship?
Solution:
```elixir prepare fn query, _ -> case query.arguments[:visibility] do :exposed -> Ash.Query.load(query, data: Ash.Query.filter(Data, parent(client_data.exposed?))) end...
Jump to solution
12 Replies
Sienhopist
SienhopistOP•3mo ago
Here's an example of my relationship and read action on my Client resource
# Relationship
many_to_many :data, MyApp.Data do
public? true
join_relationship :client_data
through MyApp.Client2Data

filter expr(
cond do
^arg(:visibility) == :exposed -> parent(client_data.exposed?)
^arg(:visibility) == :available -> not parent(client_data.exposed?)
true -> true
end
)

source_attribute :client_id
source_attribute_on_join_resource :client_id
destination_attribute_on_join_resource :data_id
could_be_related_at_creation? true
end

# Action
read :clients_with_exposed_data do
argument :visibility, :atom do
constraints one_of: [:exposed, :available, :both]
allow_nil? false
public? true
default :exposed
end

prepare build(strict_load: [:data])

filter expr(exists(data, parent(client_data.exposed?)))
end
# Relationship
many_to_many :data, MyApp.Data do
public? true
join_relationship :client_data
through MyApp.Client2Data

filter expr(
cond do
^arg(:visibility) == :exposed -> parent(client_data.exposed?)
^arg(:visibility) == :available -> not parent(client_data.exposed?)
true -> true
end
)

source_attribute :client_id
source_attribute_on_join_resource :client_id
destination_attribute_on_join_resource :data_id
could_be_related_at_creation? true
end

# Action
read :clients_with_exposed_data do
argument :visibility, :atom do
constraints one_of: [:exposed, :available, :both]
allow_nil? false
public? true
default :exposed
end

prepare build(strict_load: [:data])

filter expr(exists(data, parent(client_data.exposed?)))
end
The idea is to load all clients that have any exposed data, then in each of those clients to load only the exposed data in their data field The problem is that I don't know how to pass the argument in the action to the ^arg/1 in the many to many filter. The DSL documentation for many_to_many.filter says that I should be able to use ^arg/1 Instead, I get this SQL generated for the cond
CASE WHEN NULL = $1 THEN p1."exposed?"::boolean WHEN NULL = $2 THEN NOT (p1."exposed?"::boolean) ELSE $3::boolean END
CASE WHEN NULL = $1 THEN p1."exposed?"::boolean WHEN NULL = $2 THEN NOT (p1."exposed?"::boolean) ELSE $3::boolean END
Admittedly the argument shown here doesn't make much sense given the name and purpose of the action, but I only put it there to have a default value for my argument to see if that would fix the query but it did not. And I don't want to hard code the filter in the relationship to always choose exposed data because another action is supposed to provide the visibility argument If I can't use the arg in the relationship filter, then I think the only option is to define multiple many to many relationships with the same attributes, but different filters and choose which one to load in different actions
ken-kost
ken-kost•3mo ago
yeah, I don't think you can use arguments in relationship. 🤔 I think you could define 3 many to many relationships that have the appropriate filters. and then based on the argument load one respectively in the read action, maybe in a prepare. or move the whole logic in the read action, should also be possible. not sure, just an idea. Only Zach will know know. 📿
Sienhopist
SienhopistOP•3mo ago
I think you can only load them or not in a read action, not limit the ones that are returned Ideally I'd like to avoid repeating my many to many definitions with only a one line difference if possible 😅
ken-kost
ken-kost•3mo ago
you can have a custom query in load. you could make a case in prepare that does a different query filter on through relationship based on the argument.
Chaz Watkins
Chaz Watkins•3mo ago
field policies could help here instead of handling the filtering with the read actions. https://hexdocs.pm/ash/policies.html#field-policies as well as policies in general. for read actions policies will apply a filter and field policies gives you further control if they have access to a record but a subset of fields
ZachDaniel
ZachDaniel•3mo ago
There are some options. You can filter using arguments when you load them But the simplest way is to add more relationships
many_to_many :exposed_data, MyApp.Data do
public? true
join_relationship :client_data
through MyApp.Client2Data
filter expr(parent(client_data.exposed?))
source_attribute :client_id
source_attribute_on_join_resource :client_id
destination_attribute_on_join_resource :data_id
could_be_related_at_creation? true
end
many_to_many :exposed_data, MyApp.Data do
public? true
join_relationship :client_data
through MyApp.Client2Data
filter expr(parent(client_data.exposed?))
source_attribute :client_id
source_attribute_on_join_resource :client_id
destination_attribute_on_join_resource :data_id
could_be_related_at_creation? true
end
Sienhopist
SienhopistOP•3mo ago
What would this look like? I think doing prepare build(load: [:data]) is already going to select them all
ZachDaniel
ZachDaniel•3mo ago
WDYM select them all? Oh, sorry
Solution
ZachDaniel
ZachDaniel•3mo ago
prepare fn query, _ ->
case query.arguments[:visibility] do
:exposed -> Ash.Query.load(query, data: Ash.Query.filter(Data, parent(client_data.exposed?)))
end
end
prepare fn query, _ ->
case query.arguments[:visibility] do
:exposed -> Ash.Query.load(query, data: Ash.Query.filter(Data, parent(client_data.exposed?)))
end
end
ZachDaniel
ZachDaniel•3mo ago
something like that
Sienhopist
SienhopistOP•3mo ago
Thanks. I ended up with this at the end
defp filter_data(%Ash.Query{arguments: %{} = arguments} = query, _context) do
require Ash.Query

visibility = Map.get(arguments, :visibility, :exposed)

data_query = Data |> Ash.Query.select([:field1, :field2]) |> Ash.Query.load(:my_calculation)

data_filter =
case visibility do
:exposed -> Ash.Query.filter(data_query, parent(client_data.exposed?))
:available -> Ash.Query.filter(data_query, not parent(client_data.exposed?))
:both -> data_query
end

Ash.Query.load(query, data: data_filter)
end
defp filter_data(%Ash.Query{arguments: %{} = arguments} = query, _context) do
require Ash.Query

visibility = Map.get(arguments, :visibility, :exposed)

data_query = Data |> Ash.Query.select([:field1, :field2]) |> Ash.Query.load(:my_calculation)

data_filter =
case visibility do
:exposed -> Ash.Query.filter(data_query, parent(client_data.exposed?))
:available -> Ash.Query.filter(data_query, not parent(client_data.exposed?))
:both -> data_query
end

Ash.Query.load(query, data: data_filter)
end
Sienhopist
SienhopistOP•3mo ago
It combines everything discussed so far. An argument to determine which related Data to load based on a value in the join resource without creating additional many to many relationships, and strictly loading specific fields and calculations of the related Data

Did you find this page helpful?