dewetblomerus

dewetblomerus OP

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.

First 8 of 8 Posts Switch mode

zachdaniel

zachdaniel

Creator of Ash

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

dewetblomerus

dewetblomerus OP

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.

dewetblomerus

dewetblomerus OP

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.

zachdaniel

zachdaniel

Creator of Ash

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

zachdaniel

zachdaniel

Creator of Ash

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

dewetblomerus

dewetblomerus OP

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.

zachdaniel

zachdaniel

Creator of Ash

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
dewetblomerus

dewetblomerus OP

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
— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
michallepicki
I am using Oban and occasionally, shortly after a deployment, a handful of jobs can fail because of dependency on other parts of the syst...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New

We're in Beta

About us Mission Statement