I have a belongs_to :definition relationship on a resource and can read all the resource instances belonging to a definition with:
read :by_definition_id do
argument :definition_id, :string
filter expr(definition_id == ^arg(:definition_id))
end
I figured it would be nice to have something like:
read :by_definition do
argument :definition, :map
filter expr(definition_id == ^arg(:definition)[:id])
end
However I can’t get this to compile, nor using .id or various other combinations. I just can’t seem to connect the definition argument’s id attribute to the filter expression.
You might need to do it in a preparation to get to the value
read :by_definition do
argument :definition, :map
prepare fn query, _ ->
argument_definition_id = Ash.Query.get_argument(query, :definition)[:id]
query
|> Ash.Query.filter(definition_id == ^argument_definition_id)
end
end
1 Like
Thanks @barnabasJ that got me on the right track:
require Ash.Query
read :by_definition do
argument :definition, :map
prepare fn query, _context ->
definition_id = Ash.Query.get_argument(query, :definition).id
Ash.Query.filter(query, definition_id == ^definition_id)
end
end
Perhaps better - this works if I pass in either the definition.id or definition struct itself, though I wonder if there are problems here I’m not seeing, with the type :term being too broad. If I call it with 7 it does error out more or less as expected.
read :by_definition do
argument :definition, :term
filter definition: arg(:definition)
end
Your original example actually has an interesting issue that comes down to how Elixir as a language works
filter expr(definition_id == ^arg(:definition)[:id])
maps to this
filter expr(definition_id == ^(arg(:definition)[:id]))
But that isn’t what you want. thing[:id] is a valid expression in Ash’s expression syntax. But to use it on something pinned, you need to add parenthesis
filter expr(definition_id == (^arg(:definition))[:id])
As for accepting multiple types, Ash has a built in union type for that. I wouldn’t suggest using :term in general, but it is there to be an escape hatch for cases where it matters.
read :by_definition do
argument :definition, :union do
allow_nil? false
constraints [
types: [
definition: [type: :struct, constraints: [instance_of: Definition]],
definition_id: [type: :uuid]
]
]
end
prepare fn query, _ ->
definition_id =
case query.arguments.definition do
%Ash.Union{type: :definition_id, value: definition_id} -> definition_id
%Ash.Union{type: :definition, value: %{id: definition_id}} -> definition_id
end
Ash.Query.filter(query, definition_id == ^definition_id
end
end
It is a bit verbose, but there are ways to shrink it down and/or extract it into something reusable.
Just a bit verbose
it seems like good ergonomics to also accept a struct of the right type with an id, anywhere that an id is expected.
Thanks for the Ash.Union example, I was looking at it ealier but couldn’t figure out how to use it.
Yeah, we’ve discussed adding a built in type (although a custom one could also easily be constructed), like:
argument :thing, :struct_or_id, constraints: [for: TargetResource]
But ultimately actions being well typed is pretty important, so it would always have to come from a specially typed argument, not like magically inferred from how the argument is used.
If this was something you wanted to be able to use throughout your application, you could define a custom type and a custom preparation and get pretty elegant usage:
read :by_definition do
# you can get custom short names with a configuration, so you can do this w/o anything built into ash
argument :thing, :struct_or_id, constraints: [for: TargetResource]
# you can import functions that return `{Module, opts}`, just like builtins
prepare filter_by_struct_or_id(:definition_id, :thing)
end