AE
Ash Elixir•2y ago
zorn

Trying to better understand filter in the context of AshGraph

I am trying to work through some basics of AshGraph, and in the guide it has you set up a filter with code like:
read :query_tickets do
argument :representative_id, :uuid

filter(
expr do
is_nil(^arg(:representative_id)) or representative_id == ^arg(:representative_id)
end
)
end
read :query_tickets do
argument :representative_id, :uuid

filter(
expr do
is_nil(^arg(:representative_id)) or representative_id == ^arg(:representative_id)
end
)
end
https://ash-hq.org/docs/guides/ash_graphql/latest/graphql-generation#filter-data-with-arguments I'm having trouble understanding what is going on here. I am particularly confused about the is_nil check and why this is an or. When I run a query like:
query($representativeId: ID) {
listTickets(representativeId: $representativeId) {
id
representativeId
}
}

{"representativeId": "b6b74a04-a49f-43a5-87fb-bc1a91ad723f"}
query($representativeId: ID) {
listTickets(representativeId: $representativeId) {
id
representativeId
}
}

{"representativeId": "b6b74a04-a49f-43a5-87fb-bc1a91ad723f"}
It works as expected (I only get back tickets with that representativeId. But then, when I try:
query($representativeId: ID) {
listTickets(representativeId: $representativeId) {
id
representativeId
}
}

{"representativeId": null}
query($representativeId: ID) {
listTickets(representativeId: $representativeId) {
id
representativeId
}
}

{"representativeId": null}
I get back all tickets, those with a null representativeId and those with a proper id. My code is here in case that helps provide context: https://github.com/zorn/helpdesk-elixir-api
11 Replies
zorn
zornOP•2y ago
To try to add clarity: How would I go about building a filter so that, if the incoming graph query was explicitly asking for {"representativeId": null} I would be able to return a list of tickets where that was true. I feel like I am missing the concept of "did they include representativeId in the graph query at all, and then if they did what was its value, .. null being a valid value.
ZachDaniel
ZachDaniel•2y ago
The is_nil(^arg(:representative_id)) is talking about the argument being nil A clearer statement would be:
prepare fn query, _ ->
if query.arguments[:representative_id] do
Ash.Query.filter(query, representative_id == ^query.arguments.representative_id)
else
query
end
end
prepare fn query, _ ->
if query.arguments[:representative_id] do
Ash.Query.filter(query, representative_id == ^query.arguments.representative_id)
else
query
end
end
So if they provide a representative_id we return only those and if they do not, we return everything
zorn
zornOP•2y ago
prepare goes in exp or filter ? nevermind I think I see.. it's a DSL on read. https://ash-hq.org/docs/dsl/ash-resource#actions-read-prepare
ZachDaniel
ZachDaniel•2y ago
Yep! The way that ash expressions work, they are eagerly evaluated when possible So in the case of is_nil(^arg(:representative_id)) or representative_id == ^arg(:representative_id) if the argument :representative_id is nil, then the whole statement is true and no filter is applied This would be the same behavior if they weren't eagerly evaluted, but its an optimization that they are
zorn
zornOP•2y ago
Is it a fair assumption that the expressions in exp have the be guard-like? I tried just using a simple
prepare fn query, _ ->
dbg(query)
end
prepare fn query, _ ->
dbg(query)
end
and I'm seeing representative_id show up as a nil argument evenwhen the graphql query does not include it.
[info] POST /playground
[debug] Processing with Absinthe.Plug.GraphiQL
Parameters: %{"query" => "query {\n listTickets {\n id\n subject\n status\n representativeId\n }\n}\n", "variables" => nil}
Pipelines: [:graphql]
[debug] ABSINTHE schema=HelpdeskWeb.Schema variables=%{}
---
query {
listTickets {
id
subject
status
representativeId
}
}
---
[(helpdesk 0.1.0) lib/helpdesk/support/resources/ticket.ex:56: Helpdesk.Support.Ticket.preparation_0_generated_FB8BAA3AC8B6844F0DF189EACB552F00/2]
query #=> #Ash.Query<
resource: Helpdesk.Support.Ticket,
arguments: %{representative_id: nil, status: nil},
select: [:representative_id, :status, :subject, :id]
>
[info] POST /playground
[debug] Processing with Absinthe.Plug.GraphiQL
Parameters: %{"query" => "query {\n listTickets {\n id\n subject\n status\n representativeId\n }\n}\n", "variables" => nil}
Pipelines: [:graphql]
[debug] ABSINTHE schema=HelpdeskWeb.Schema variables=%{}
---
query {
listTickets {
id
subject
status
representativeId
}
}
---
[(helpdesk 0.1.0) lib/helpdesk/support/resources/ticket.ex:56: Helpdesk.Support.Ticket.preparation_0_generated_FB8BAA3AC8B6844F0DF189EACB552F00/2]
query #=> #Ash.Query<
resource: Helpdesk.Support.Ticket,
arguments: %{representative_id: nil, status: nil},
select: [:representative_id, :status, :subject, :id]
>
ZachDaniel
ZachDaniel•2y ago
it would need to be something like the following to provide a value
query {
listTickets(representativeId: "someRepresentativeId") {
...
}
}
query {
listTickets(representativeId: "someRepresentativeId") {
...
}
}
Expressions do not need to be guard-like They are their own thing
zorn
zornOP•2y ago
My current observation is that both:
query {
listTickets(representativeId: null) {
id
representativeId
}
}
query {
listTickets(representativeId: null) {
id
representativeId
}
}
and
query {
listTickets {
id
representativeId
}
}
query {
listTickets {
id
representativeId
}
}
Both debug like:
query #=> #Ash.Query<
resource: Helpdesk.Support.Ticket,
arguments: %{representative_id: nil},
select: [:representative_id, :id]
>
query #=> #Ash.Query<
resource: Helpdesk.Support.Ticket,
arguments: %{representative_id: nil},
select: [:representative_id, :id]
>
I would have expected that when the argument was not present in the graph query it would not be present in the Ash.Query and would debug more like:
query #=> #Ash.Query<
resource: Helpdesk.Support.Ticket,
arguments: %{},
select: [:representative_id, :id]
>
query #=> #Ash.Query<
resource: Helpdesk.Support.Ticket,
arguments: %{},
select: [:representative_id, :id]
>
I guess what I'm missing is that there is no way to define representative_id as an optional argument in the read action.
ZachDaniel
ZachDaniel•2y ago
That looks like a bug 🙂 Trying to figure out why that would be the case the way we currently handle it is like so:
def set_query_arguments(query, action, arg_values) do
action =
if is_atom(action) do
Ash.Resource.Info.action(query.resource, action)
else
action
end

action.arguments
|> Enum.reject(& &1.private?)
|> Enum.reduce(query, fn argument, query ->
case Map.fetch(arg_values, argument.name) do
{:ok, value} ->
Ash.Query.set_argument(query, argument.name, value)

_ ->
query
end
end)
end
def set_query_arguments(query, action, arg_values) do
action =
if is_atom(action) do
Ash.Resource.Info.action(query.resource, action)
else
action
end

action.arguments
|> Enum.reject(& &1.private?)
|> Enum.reduce(query, fn argument, query ->
case Map.fetch(arg_values, argument.name) do
{:ok, value} ->
Ash.Query.set_argument(query, argument.name, value)

_ ->
query
end
end)
end
So if the key isn't present it shouldn't be set as an argument Can you open an issue on ash_graphql about this please? I'll investigate tonight or tomorrow
zorn
zornOP•2y ago
GitHub
When a graph query has no arguments, I expect the Ash.Query objec...
I'm new to AshGraph, and while tinkering, I discovered an unexpected behavior where even when I do not define any arguments in the graph query the Ash.Query always contains the field name in th...
zorn
zornOP•2y ago
To close this thread, @Zach Daniel saw the error source of the confusion which was an incorrect Inspect prootocol in the main ash project. The ticket was updated and has the needed links. In my own project I have the behavior I am looking for with:
read :query_tickets do
argument :representative_id, :uuid

prepare fn query, _ ->
query =
case Map.fetch(query.arguments, :representative_id) do
{:ok, nil} ->
Ash.Query.filter(query, is_nil(representative_id))

{:ok, representative_id} ->
Ash.Query.filter(query, representative_id == ^representative_id)

:error ->
query
end
end
end
read :query_tickets do
argument :representative_id, :uuid

prepare fn query, _ ->
query =
case Map.fetch(query.arguments, :representative_id) do
{:ok, nil} ->
Ash.Query.filter(query, is_nil(representative_id))

{:ok, representative_id} ->
Ash.Query.filter(query, representative_id == ^representative_id)

:error ->
query
end
end
end

Did you find this page helpful?