Reuse filters or actions in other action?

Is there a way to make reusable actions/filters?

I have this now

    read :next_two_days do
      now = Timex.now()
      start_time = now
      end_time = Timex.shift(now, days: +2)
      filter expr(start_time >= ^start_time and end_time <= ^end_time)
    end

    read :last_two_days do
      now = Timex.now()
      start_time = Timex.shift(now, days: -2)
      end_time = now
      filter expr(start_time >= ^start_time and end_time <= ^end_time)
    end

Can i make a reusable filter ? Or call another action?

EDIT: ignore the fact that this is hard to test given the action is not pure

1 Like

Hi @jarlah :wave:

I feel like the first thing I should point out is that your now, start_time and end_time will be evaluated at compile time and never change at runtime.

You should instead do something like this:

read :next_two_days do
  argument :start_time, :datetime, private?: true, default: &Timex.now/0
  argument :end_time, :datetime, private?, true, default: fn -> Timex.shift(Timex.now(), days: +2) end
  filter expr(start_time >= ^arg(:start_time) and end_time <= ^arg(:end_time))
end

but having said that I think it see a nice easy way to make this reusable:

read :next_two_days do
   manual fn _, _, _, _ ->
     start_time = Timex.now()
     end_time = Timex.shift(start_time, days: +2)
    # provided you added the code_interface, or you could construct a new query instead.
     __MODULE__.within_datetime_range(%{start_time: start_time, end_time: end_time)
   end
end

read :last_two_days do
   manual fn _, _, _, _ ->
     end_time = Timex.now()
     start_time = Timex.shift(end_time, days: -2)
     __MODULE__.within_datetime_range(%{start_time: start_time, end_time: end_time)
   end
end

read :within_datetime_range do
  argument :start_time, :datetime
  argument :end_time, :datetime
  filter expr(start_time >= ^arg(:start_time) and end_time <= ^arg(:end_time))
end
3 Likes

I would suggest making filters reusable with calculations generally, and in this specific case, use the from_now and ago expressions.

read :next_two_days do
  filter expr(in_date_range(datetime: expr(from_now(2, :day))))
end

read :last_two_days do
  filter expr(in_date_range(datetime: expr(ago(2, :day))))
end

calculate :in_date_range, :boolean, expr(start_time <= ^arg(:datetime) and end_time >= ^arg(:datetime)) do
  private? true
  argument :datetime, :datetime, allow_expr?: true # allow_expr allows nested expressions to be provided
end

Another thing to emphasize is that there are tools for regular functional composition. filter in the action, i.e

filter expr(foo == bar)

is a short-hand that in complex cases is often inconvenient compared to using a preparation (the standard way of customizing the query of an action.

For example:

read :next_two_days do
  prepare fn query, _ -> 
    filter_between(query, Timex.shift(Timex.now(), days: 2))
  end
end

read :last_two_days do
  prepare fn query, _ -> 
    filter_between(query, Timex.shift(Timex.now(), days: -2))
  end
end

defp filter_between(query, datetime) do
  Ash.Query.filter(query, start_time <= ^datetime and end_time >= ^datetime)
end
2 Likes

As always @zachdaniel has a better idea than I do :joy:

2 Likes

I like the first approach best. But its nice to know we have escape hatches like the last one.