How to filter using a code interface

I am trying to create a code interface that is powered by a query that will grow and evolve over time.

To just play around with Ash queries I tried to start easy and return all the resources where the attribute word is "red".

This code works:

require Ash.Query

Red.Practice.Card
|> Ash.Query.filter(word == "red")
|> Red.Practice.read!()

But the above is not a nice code interface.

Under actions I have tried to create the following:

    read :next do
      # filter(is_nil([:retry_at]))
      # filter(expr([:retry_at] == "NULL"))
      # filter([:word] == "red")
    end

None of those worked.

I am clearly guessing here and not finding any examples in the documentation. I was looking here but it does not seem like any of these examples are for using inside of a resource. It looks like it is meant for building queries like I already have working.

Bonus round:
Ultimately I need a next action that filters to actor: user, if there is a resource with a retry_at in the past, return the one with the oldest retry_at date. If not, return a resource with retry_at: nil and the oldest created_at. I don’t need that code written for me, but which documentation or example should I be looking at for composing a query inside of a resource action and exposing it through a code interface.

There are a couple elements that you’re going to need for this.

The expressions guide shows examples of the kinds of things you can do with expr. In your case what you’re looking for is

filer expr(is_nil(retry_at))

What you might end up wanting to do for simplicities sake is writing an action that uses an after_action hook to detect that no results were returned and tries another query (your second condition).

prepare fn query, _ -> 
  Ash.Query.after_action(query, fn 
    query, [] ->
      # call another read action here
    _query, results ->
      {:ok, results}
  end
end

With that said, you may also be able to accomplish this with sorts and calculations. For example, you could have a calculation that returns the retry_at as long as its in the past or, nil otherwise, something like:

calculate :elligible_retry_at, :datetime, expr(
  if retry_at < now() do
     retry_at
  else
     nil
  end
)

# add a similar calculation that is "created_at only if retry_at is nil"
Ash.Query.sort(query, elligible_retry_at: :asc_nils_last, other_thing: :asc_nils_last)

I think that combined with a limit of 1 would get you what you need.

Docs for sort/limit is here: Ash.Query — ash v3.29.3

Docs for calculations here: Calculations — ash v3.29.3

Thanks, Zac :tada:. This works to return all the instances of the resource where the attribute is nil:

    read :next do
      filter(expr(is_nil(retry_at)))
    end

I’ll report back when I find time to try the rest.

I was able to make the after_action work. Here are my code_interface and action blocks:

  code_interface do
    define_for Red.Practice

    define :create, action: :create

    define :next, action: :next, args: [:user_id]
    define :oldest_untried_card, action: :oldest_untried_card, args: [:user_id]
  end

  actions do
    defaults [:read, :update]

    create :create do
      change relate_actor(:user)
    end

    read :next do
      argument :user_id, :integer do
        allow_nil? false
      end

      get? true

      prepare build(limit: 1, sort: [retry_at: :asc])

      filter expr(retry_at < now() and user_id == ^arg(:user_id))

      prepare fn query, _ ->
        Ash.Query.after_action(query, fn
          query, [] ->
            dbg("No cards found with retry_at < now")
            Red.Practice.Card.oldest_untried_card(query.arguments.user_id)

          _query, results ->
            dbg("A card was found with retry_at < now")
            {:ok, results}
        end)
      end
    end

    read :oldest_untried_card do
      argument :user_id, :integer do
        allow_nil? false
      end

      prepare build(limit: 1, sort: [created_at: :asc])

      filter expr(is_nil(retry_at) and user_id == ^arg(:user_id))
    end
  end

One problem is that I was unable to figure out how to pass an actor to a read action. I was also unable to use Ash.get_actor() outside of the prepare fn query block, so I just made the code interface to accept a user_id but this interface would only ever be used by the actor that owns the resources being returned, so I would like for it not to receive a user_id as an argument.

The actor should be available as an option to Ash.Query.for_read, does that not work?

And then it will be in context.actor (context is the second argument to the prepare function)

I tried this. I went down this path for a while for the purposes of learning, but this code lives outside of the resource and I would want the code interface to expose a single function to anything outside of the resource.

# outside of the resource
Red.Practice.Card
|> Ash.Query.for_read(:next, %{}, actor: user)
|> Red.Practice.read_one!()

The above is valid code and returns a result, but it does not filter by actor automatically.

Which made me think I still need to filter by actor in the filter expression, but I do not know how to access the actor there.

I tried this, which is invalid: filter expr(retry_at < now() and user_id == ^context.actor)

The prepare function had an after_action which ran after we already queried DB results belonging to other users, so it felt too late to start filtering there.

I also tried the following to see if I can add the filter in a before_action which was valid code, but the before_action never ran:

# inside the action
prepare fn query, context ->
        Ash.Query.before_action(query, fn
          query, results ->
            dbg("before action")
            dbg(query)
            {:ok, results}
        end)

        Ash.Query.after_action(query, fn
          query, [] ->
            dbg("No cards found with retry_at < now")
            Red.Practice.Card.oldest_untried_card()

          _query, results ->
            dbg("A card was found with retry_at < now")
            {:ok, results}
        end)
      end

I am okay with just passing the user_id as an argument to the code interface. It would be used like this from outside of the resource: Red.Practice.Card.next(user.id).

I will probably never create a JSON or GraphQL Api for this app. But I suspect that if I do, I would need to find a way to just use the auth context and not pass in an argument here.

But if @zachdaniel or anyone else is willing to keep helping me find the even-more-better way to do this, I’m open to keep trying things.

Referencing the actor from a template looks like this:

filter expr(retry_at < now() and user_id == ^actor(:id))

Your before action hook never ran because you didn’t return the new query containing the before_action hook. It would look something like this:

# inside the action
prepare fn query, context ->
  query
  |> Ash.Query.before_action(fn query, results ->
    dbg("before action")
    dbg(query)
    {:ok, results}
  end)
  |> Ash.Query.after_action(fn 
    query, [] ->
      dbg("No cards found with retry_at < now")
      Red.Practice.Card.oldest_untried_card()

    _query, results ->
      dbg("A card was found with retry_at < now")
      {:ok, results}
  end)
end

By Golly it works :tada::partying_face::bottle_with_popping_cork:

If anyone else is still following along and wants a complete example, here is the now-beautiful interface that can be used elsewhere in the application:

Red.Practice.Card.next(actor: user)

And here is the resource:

defmodule Red.Practice.Card do
  use Ash.Resource,
    data_layer: AshPostgres.DataLayer

  code_interface do
    define_for Red.Practice

    define :create, action: :create

    define :next, action: :next
    define :oldest_untried_card, action: :oldest_untried_card
  end

  actions do
    defaults [:read, :update]

    create :create do
      change relate_actor(:user)
    end

    read :next do
      get? true

      prepare build(limit: 1, sort: [retry_at: :asc])

      filter expr(retry_at < now() and user_id == ^actor(:id))

      prepare fn query, context ->
        Ash.Query.after_action(query, fn
          query, [] ->
            dbg("No cards found with retry_at < now")
            Red.Practice.Card.oldest_untried_card(actor: context.actor)

          _query, results ->
            dbg("A card was found with retry_at < now")
            {:ok, results}
        end)
      end
    end

    read :oldest_untried_card do
      prepare build(limit: 1, sort: [created_at: :asc])

      filter expr(is_nil(retry_at) and user_id == ^actor(:id))
    end
  end

  attributes do
    integer_primary_key :id

    attribute :word, :string, allow_nil?: false
    attribute :tried_at, :utc_datetime, allow_nil?: true
    attribute :retry_at, :utc_datetime, allow_nil?: true
    attribute :correct_streak, :integer, allow_nil?: false, default: 0

    create_timestamp :created_at
    create_timestamp :updated_at
  end

  identities do
    identity :unique_word, [:word, :user_id]
  end

  relationships do
    belongs_to :user, Red.Accounts.User,
      attribute_writable?: true,
      attribute_type: :integer,
      allow_nil?: false
  end

  postgres do
    table "cards"
    repo Red.Repo
  end
end