How to filter an array attribute to find at least one instance (using map syntax)

I have Document resource with attribute :labels, {:array, :string}. I’m trying to build a filter that will match a single label.

E.g.

%{labels: ["foo"]} 

This returns documents with on a single “Foo” label. I want all documents that have at least “Foo”, and other labels. I have this example from the source

               |> Ash.Query.filter(title in cool_titles)

But I can’t seem to get this reverse format to work in a map-based filter. (I’m using that because I’m filtering with user input, so wanted to keep using Ash.Filter.parse_input/2).

Thanks for any advice.

I might have answered myself here.

The docs warn about using Ash.Filter.parse_input/2:

Security Concerns
If you are using a map with string keys, it is likely that you are parsing input. It is important to note that, instead of passing a filter supplied from an external source directly to Ash.Query.filter/2, you should call Ash.Filter.parse_input/2. This ensures that the filter only uses public attributes, relationships, aggregates and calculations, honors field policies and any policies on related resources.

But looking at the source for this function, it looks like this only does checking for the filter keys. That is, if I’m searching on a public attribute (Labels) using user input for filter values, then this function doesn’t provide anything and I’m fine to just use an expression directly.

Is that right?

1 Like

It depends :slight_smile:

When you have things like field policies or related resource policies, we can tell when a field from parse_input/2 is used and will use that to adjust the filters to scope them to what a user can see. If you do Ash.Query.filter(related.field in ^user_suppleid_list), we don’t apply a filter that “the related thing is visible to the user in the first place”, nor replace the value with the expression corresponding to field policies.

My recommendation is to create a calculation on your resource called has_label, and use that.

calculate :has_label, :boolean, expr(^arg(:label) in labels) do
  argument :label, :string, allow_nil?: false
end

then you can do:

Ash.Filter.parse_input!(Resource, has_label: %{input: %{label: "foo"}, eq: true}) 

The docs should definitely improve in this regard. Especially documenting how to do calculations with input (that input is a special field to provide arguments to the calculation)

Something we could also do to make this better would be to make an input_ref template helper, so you could say Ash.Query.filter(^label in ^input_ref(:labels)) to get the same protections around field access while using the expression syntax.

Thanks for the pointers :heart:

(I updated the PR to reflect all this, and the calculation works nicely)

1 Like