ash-project/ash

Custom predicates & operators

Closed this issue · 4 comments

These would be exposed to the expression syntax in code, and can be made available over api extensions as well.

I'm thinking something like this:

  1. support a custom configured list of functions to support. Actually it would be two lists, a list of functions that take 2 arguments, the first one a field reference and the second one a value. The second list would be a list of functions that produce values, and should then be "predicated on". i.e
config :ash, Ash.Filter,
  expose_predicates: %{
    module: SomeMod,
  }

config :ash, Ash.Filter,
  expose_functions: %{
    module: SomeMod
  }

Extensions like AshGraphql that bring in filter expressions automatically simply add these modules to the lists they are already getting, and bobs your uncle. however this will be almost useless without #2

  1. The ability to define custom functions and operators globally, with a way to determine what the expression actually becomes for any given data layer. i.e
defmodule MyApp.Predicates.ILike do
  use Ash.CustomOperator,
    name: :ilike

  def to_expression(AshPostgres.DataLayer, left, right) do
    expr(fragment("(? ILIKE ?)", left, right))
  end

  def evaluate(left, right) when is_binary(left) and is_binary(right) do
    # this is wrong, because ilike supports match patterns, but this is just an example. You'd actually return `:unknown` here, i.e I can't do this in elixirland
    {:ok, String.downcase(left) == String.downcase(right)}
  end
end

Neither of those things are very complex. And those custom operators/functions would be available in your in-code expressions as well. i.e given the above config, you'd get:

Ash.Query.filter(query, name ilike "%fred%")

and in graphql, filter: {name: {ilike: "%fred%"}}

With that you can basically get any kind of filtering you want.

I'm happy to take this one on @zachdaniel

@zachdaniel As a first step towards this, could we look at making a single datalayer extensible?
Eg for AshPostgres, I think we could looks for user-defined extensions in the DataLayer.functions callback:

https://github.com/ash-project/ash_postgres/blob/fb8a13f33d36d84b58559b4345e3a01606c00fd2/lib/data_layer.ex#L597-L611

And add a clause to do_dynamic_expr to delegate to the appropriate module when an unknown function struct is encountered:

 defp do_dynamic_expr(
         query,
         %custom_function{__function?__: true} = func,
         bindings,
         embedded?,
         type
       ) do
  custom_function.to_ecto_dynamic(func, query, bindings, embedded?, type)
end

That is much better :D I like that idea a lot, lets go for it.

This has been done in 3.0