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.

https://hexdocs.pm/ash/expressions.html

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 v2.15.19

Docs for calculations here: Calculations — ash v2.15.19

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

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
2 Likes