Read action by related struct

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 :slight_smile: 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